[В коде урезаны методы и поля, не относящиеся к делу]
Model.cs
...
namespace Timetronome
{
public class Model : INotifyPropertyChanged
{
...
private string toFilePath = "click.wav";
Thread timerThread;
Thread clickerThread;
MediaPlayer mediaPlayer;
MediaPlayer mediaChecker;
public Model (int receivedTempo, int receivedTime)
{
SettedTempo = receivedTempo;
SettedTime = receivedTime;
clickerThread = new Thread(new ThreadStart(Clicker));
timerThread = new Thread(new ThreadStart(Timer));
//CheckMediaFile(toFilePath);
clickerThread.Start();
timerThread.Start();
}
public int EstimateTime
{
get => estimateTime;
private set
{
estimateTime = value;
OnPropertyChanged();
}
}
public bool IsMediaFailed
{
get => isMediaFailed;
private set
{
isMediaFailed = value;
OnPropertyChanged();
}
}
public void ToogleMetronomeState(int receivedTempo, int receivedTime)
{
//CloseMediaChecker();
if (!IsMetronomeRunned)
{
SettedTempo = receivedTempo;
SettedTime = receivedTime;
RunMetronome();
}
else
{
StopMetronome();
}
}
private void Clicker()
{
mediaPlayer = new MediaPlayer();
mediaPlayer.MediaFailed += NotifyMediaFailed;
mediaPlayer.Open(new Uri(toFilePath, UriKind.Relative));
int delay;
while (!IsClosingApp)
{
ThreadWaiting();
delay = 60000 / SettedTempo;
while (!IsClosingApp && IsMetronomeRunned)
{
mediaPlayer.Stop();
mediaPlayer.Play();
ThreadDelay(delay);
}
}
mediaPlayer.Close();
}
private void Timer()
{
while (!IsClosingApp)
{
ThreadWaiting();
EstimateTime = SettedTime;
while (!IsClosingApp && IsMetronomeRunned && (EstimateTime > 0))
{
ThreadDelay(60000);
EstimateTime--;
}
IsMetronomeRunned = false;
}
}
private void CheckMediaFile(string toFilePath)
{
mediaChecker = new MediaPlayer();
mediaChecker.MediaFailed += NotifyMediaFailed;
mediaChecker.Open(new Uri(toFilePath, UriKind.Relative));
}
private void CloseMediaChecker() => mediaChecker?.Close();
private void NotifyMediaFailed(object sender, ExceptionEventArgs e) => IsMediaFailed = true;
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private bool IsClosingApp [...]
public bool IsMetronomeRunned [...]
public int SettedTempo [...]
public int SettedTime [...]
private void RunMetronome() [...]
private void StopMetronome() [...]
private void ThreadDelay(int delay) [...]
private void ThreadWaiting() [...]
private void ThreadInterrupt(Thread threadVariable) [...]
public void CloseApp() [...]
}
}
ViewModel.cs
...
namespace Timetronome
{
public class ViewModel : INotifyPropertyChanged
{
...
public ViewModel(string fromViewTempo, string fromViewTime)
{
FromViewTempo = ParseString(fromViewTempo, FromViewTempo);
FromViewTime = ParseString(fromViewTime, FromViewTime);
model = new Model(FromViewTempo, FromViewTime);
model.PropertyChanged += ModelNotify;
}
public int EstimateTime { get => model.EstimateTime; }
public bool IsMediaFailed { get => model.IsMediaFailed; }
private void ModelNotify(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e.PropertyName);
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int FromViewTempo [...]
private int FromViewTime [...]
public bool IsMetronomeRunned [...]
public int SettedTempo [...]
public int SettedTime [...]
private int ParseString(string inputString, int previousIntVariableValue) [...]
public void ToggleMetronomeState(string fromViewTempo, string fromViewTime) [...]
public void CloseApp() [...]
}
}
MainWindow.xaml.cs
...
namespace Timetronome
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
...
public MainWindow()
{
InitializeComponent();
StartStopButtonText = "Start";
viewModel = new ViewModel("120", "5");
DataContext = viewModel;
this.Closing += MainWindowClosing;
viewModel.PropertyChanged += ViewModelNotify;
}
public string StartStopButtonText
{
get => startStopButtonText;
private set
{
startStopButtonText = value;
OnPropertyChanged();
}
}
private void ViewModelNotify(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsMetronomeRunned" || e.PropertyName == "EstimateTime")
ChangeStartStopButtonText();
if (e.PropertyName == "IsMediaFailed" && viewModel.IsMediaFailed)
ShowMediaFailedMessage();
}
private void ChangeStartStopButtonText()
{
if (viewModel.IsMetronomeRunned)
StartStopButtonText = "Est. time:" + Environment.NewLine + viewModel.EstimateTime + " min";
else
StartStopButtonText = "Start";
}
private void ShowMediaFailedMessage()
{
MessageBox.Show("click.wav is absent or broken!");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged ([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void StartStopButtonClick(object sender, RoutedEventArgs e) [...]
private void MainWindowClosing(object sender, System.ComponentModel.CancelEventArgs e) [...]
}
}
MainWindow.xaml
<Window
x:Name="MainWindowClass"
...
<Grid>
...
<Button x:Name="StartStopButton" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" Click="StartStopButtonClick" >
<TextBlock x:Name="StartStopButtonTextBlock" TextWrapping="Wrap" TextAlignment="Center"
Text="{Binding StartStopButtonText, ElementName=MainWindowClass, UpdateSourceTrigger=PropertyChanged}"/>
</Button>
</Grid>
</Window>
Пытаюсь "поднять" сообщение от
mediaPlayer из метода
Clicker (из Model.cs в MainWindow.xaml.cs):
в потоке
clickerThread подписываюсь методом
NotifyMediaFailed => передаю в свойство
IsMediaFailed значение
true => вызываю метод
OnPropertyChanged, который отправляет сообщение"наверх" (в основной поток), вместе с прочими уведомлениями.
Подобным образом работает запись в свойство
EstimateTime в методе
Timer:
в потоке
timerThread передаю в свойство
EstimateTime некоторое значение => вызываю метод
OnPropertyChanged, который отправляет сообщение"наверх".
События обрабатываются одинаково вплоть до
MainWindow.xaml.cs.
Но в первом случае событие где-то теряется.
Ключевое отличие в обработке на стороне MainWindow:
- отсутствие привязки непосредственно к какому-либо элементу для события
IsMediaFailed;
- наличие привязки к кнопке
StartStopButton через свойство
StartStopButtonText для события
EstimateTime.
Если раскомментировать в конструкторе класса
Model метод
CheckMediaFile, то вызванный им дополнительный
MediaPlayer из основного потока успешно доставит сообщение.
Резюмируя вопрос: два сторонних потока ведут запись в свойства, которые обрабатываются главным потоком, но почему это не работает в одном из случаев?