Reputation: 9591
I am encountering a major gotcha when using the MVVM architecture and I haven't seen it directly addressed or even mentioned. In a multi-threaded application, it's possible for a view to start databinding before the View Model is fully constructed, creating binding failures.
Here's is an example:
public class StocksViewModel
{
// blah blah
public IOrderedEnumerable<KeyValuePair<string, string>> AvailableStocks
{
get
{
// This could take a minute or two
return StockService.GetAvailableStocks(User);
}
}
public double Quantity
{
get => Quantity;
set
{
Quantity = value; RaisePropertyChanged(nameof(Quantity));
}
}
public double Symbol
{
get => Symbol;
set
{
Symbol = value; RaisePropertyChanged(nameof(Symbol));
}
}
}
Here's theXAML:
<ComboBox x:Name="Quantity" Grid.ColumnSpan="2" Grid.Column="2" SelectedValue="{x:Bind Mode=TwoWay, Path=ViewModel.Quantity}" DisplayMemberPath="Value" SelectedValuePath="Key" ItemsSource="{x:Bind ViewModel.AvailableStocks}" />
<ComboBox x:Name="Symbol" Grid.ColumnSpan="2" Grid.Column="2" SelectedValue="{x:Bind Mode=TwoWay, Path=ViewModel.Symbol}" DisplayMemberPath="Value" SelectedValuePath="Key" ItemsSource="{x:Bind ViewModel.AvailableStocks}" />
As you can see, the databinding can be unpredictable. If the StockService takes several seconds, the Quantity and Symbol will never get a chance to data-bind to AvailableStock.
What's the prescribed method for handling this situation?
Upvotes: 1
Views: 61
Reputation: 1301
My approach to this would be to take a different path than you have here. In the constructor for my view model, I would create a readonly ObservableCollection<KeyValuePair<string, string>>
. I would also kick off a new thread to call GetAvailableStocks
at that time. When the result returns, you can populate the ObservableCollection
. However, while the data is being retrieved, I would include a loading icon or a similar idea so the user is aware we are still gathering results.
A view model should load all the data it can and be ready to display additional data whenever it is received. If the data could be preloaded and passed into the view model, that would be even better but this can't always occur.
A code sample (excuse my formatting as I'm on mobile):
public class ViewModel
{
readonly ObservableCollection<KeyValuePair<string, string>> _availableStocks;
public ObservableCollection<KeyValuePair<string, string>> AvailableStocks
{
get => _availableStocks;
}
public bool IsExecuting { get; set; }
public ViewModel()
{
_availableStocks = new ObservableCollection<KeyValuePair<string, string>>();
IsExecuting = true;
TaskFactory.StartNew(() =>
{
var results = StockService.GetAvailableStocks(User);
foreach (var stock in results)
AvailableStocks.Add(stock);
IsExecuting = false;
}
}
}
This code is untested but I hope it will help you with your issue!
Upvotes: 1