My C# application has a data provider component that updates asynchronously in its own thread. The ViewModel classes all inherit from a base class that implements INotifyPropertyChanged
. In order for the asynchronous data provider to update properties in the View using the PropertyChanged event, I found my ViewModel became very closely coupled with the view due to the need to only raise the event from within the GUI thread!
#region INotifyPropertyChanged
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler RaisePropertyChangedEvent = PropertyChanged;
if (RaisePropertyChangedEvent!= null)
{
var propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);
// This event has to be raised on the GUI thread!
// How should I avoid the unpleasantly tight coupling with the View???
Application.Current.Dispatcher.BeginInvoke(
(Action)(() => RaisePropertyChangedEvent(this, propertyChangedEventArgs)));
}
}
#endregion
Are there any strategies for eliminating this coupling between the ViewModel and the View implementation?
EDIT 1
This answer is related and highlights the issue of updating collections. However, the proposed solution also uses the current dispatcher, which I do not want to be a concern for my ViewModel.
EDIT 2 Digging a bit deeper into the question above and I've found a link answer that does answer my question: create an Action<> DependencyProperty in the View that the View model can use to get the View (whatever that may be) to handle the dispatching where necessary.
EDIT 3
It appears the the question as asked "is moot". However, when my ViewModel exposes an Observable Collection as a property for the view to bind to (see EDIT 1), it still requires access to teh dispatcher to Add()
to the collection. For example:
App.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace MultiThreadingGUI
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
Startup += new StartupEventHandler(App_Startup);
}
void App_Startup(object sender, StartupEventArgs e)
{
TestViewModel vm = new TestViewModel();
MainWindow window = new MainWindow();
window.DataContext = vm;
vm.Start();
window.Show();
}
}
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<String> ListFromElsewhere { get; private set; }
public String TextFromElsewhere { get; private set; }
private Task _testTask;
internal void Start()
{
ListFromElsewhere = new ObservableCollection<string>();
_testTask = new Task(new Action(()=>
{
int count = 0;
while (true)
{
TextFromElsewhere = Convert.ToString(count++);
PropertyChangedEventHandler RaisePropertyChanged = PropertyChanged;
if (null != RaisePropertyChanged)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs("TextFromElsewhere"));
}
// This throws
//ListFromElsewhere.Add(TextFromElsewhere);
// This is needed
Application.Current.Dispatcher.BeginInvoke(
(Action)(() => ListFromElsewhere.Add(TextFromElsewhere)));
Thread.Sleep(1000);
}
}));
_testTask.Start();
}
}
}
MainWindow.xaml
<Window x:Class="MultiThreadingGUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="TextFromElsewhere:" />
<Label Grid.Row="0" Grid.Column="1" Content="{Binding Path=TextFromElsewhere}" />
<Label Grid.Row="1" Grid.Column="0" Content="ListFromElsewhere:" />
<ListView x:Name="itemListView" Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Path=ListFromElsewhere}">
<ListView.ItemTemplate>
<DataTemplate>
<Label Content="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
So, how do I avoid that little call to BeginInvoke? Do I have to re-invent the wheel and create a ViewModel container for the list? Or can I delegate the Add()
to the View somehow?