問題描述
多次調用 PropertyChanged 的 ViewModel 屬性 (ViewModel properties with multiple calls to PropertyChanged)
最近我一直在學習 C# 和 WPF。我正在嘗試在我正在處理的項目中使用 MVVM,只是為了保持代碼井井有條並了解它是如何工作的。
在 MVVM 中,View 上的控件綁定到 ViewModel 上的屬性,這實現 INotifyPropertyChanged。很多時候,當某個屬性被更新時,我會想要一堆其他屬性也隨之更新。
例如,我有一個上面有一個 TextBox 的 ListBox。您可以在 TextBox 中輸入內容,它會過濾 ListBox 中的內容。但在某些情況下,我還需要能夠從代碼中清除 TextBox。代碼最終看起來像這樣:
private Collection<string> _listOfStuff;
public Collection<string> FilteredList
{
get
{
if (String.IsNullOrWhiteSpace(SearchText))
{
return _listOfStuff;
}
else
{
return new Collection<string>(_listOfStuff.Where(x => x.Contains(SearchText)));
}
}
set
{
if (value != _listOfStuff)
{
_listOfStuff = value;
OnPropertyChanged("FilteredList");
}
}
}
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
if (value != _searchText)
{
_searchText = value;
OnPropertyChanged("SearchText"); // Tells the view to change the value of the TextBox
OnPropertyChanged("FilteredList"); // Tells the view to update the filtered list
}
}
}
隨著這個項目變得越來越大,這開始變得草率了。我有 6 次調用 OnPropertyChanged
的二傳手 並且越來越難以跟踪內容。有沒有更好的方法來做到這一點?
參考解法
方法 1:
First you shouldn't do potentially expensive operations in a command, then you'll be able to remove the OnPropertyChanged("FilteredList");
from your SearchText
.
So you should move that code from the getter and into it's own command and bind it from XAML (either as Command on a button or using Blends Interactivity Trigger to call it when the text fields value changes).
public ICommand SearchCommand { get; protected set; }
// Constructor
public MyViewModel()
{
// DelegateCommand.FromAsyncHandler is from Prism Framework, but you can use
// whatever your MVVM framework offers for async commands
SearchCommand = DelegateCommand.FromAsyncHandler(DoSearch);
}
public async Task DoSearch()
{
var result = await _listOfStuff.Where(x => x.Contains(SearchText)).ToListAsync();
FilteredList = new Collection<string>(result);
}
private Collection<string> _listOfStuff;
private Collection<string> _filteredList;
public Collection<string> FilteredList
{
get
{
return _filteredList;
}
set
{
if (value != _filteredList)
{
_filteredList = value;
OnPropertyChanged("FilteredList");
}
}
}
private string _searchText;
public string SearchText
{
get
{
return _searchText;
}
set
{
if (value != _searchText)
{
_searchText = value;
OnPropertyChanged("SearchText");
}
}
}
On a side note: You can also use OnPropertyChanged(nameof(FilteredList));
to have a refactor friendly version, when you rename your property all of your OnPropertyChanged
calls will be updated to. Requires C# 6.0 though, but it's compatible with older .NET Frameworks (back to 2.0), but requires Visual Studio 2015 or later
方法 2:
I tried out Assisticant on a project about a year ago. It figures out which of your properties need to raise notifications and also which are related. There is a good course for it on Pluralsight and the examples on the website are pretty good. If nothing else you could check out the source code to see how he did it.
Also some good suggestions from Change Notification in MVVM Hierarchies.
They mentioned: Use an attribute ‑> e.g. [DependsUpon(nameof(Size))]
and
Josh Smith's PropertyObserver
Could put the raise property change calls in a method if you just need to raise the same notifications every time.
方法 3:
For anyone searching for a good solution to this type of problem: Check out ReactiveUI.
It is a framework based on Reactive Extensions (Rx), with the idea that you model this type of dependencies between properties explicitly, without a jungle of RaisePropertyChanged(..)
.
Specifically check out the ObservableAsPropertyHelper (sometimes called OAPH).
方法 4:
You should only raise OnPropertyChanged
in the setter of the property itself.
A cleaner implementation of your ViewModel can be:
private Collection<string> _listOfStuff;
private Collection<string> _filteredList;
public Collection<string> FilteredList
{
get
{
return _filteredList;
}
set
{
if (value != _filteredList)
{
_filteredList = value;
OnPropertyChanged("FilteredList");
}
}
}
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
if (value != _searchText)
{
_searchText = value;
OnPropertyChanged("SearchText");
FilteredList = new Collection<string>(_listOfStuff.Where(x => x.Contains(SearchText)));
}
}
}
(by Patrick A.、Tseng、Joseph Evensen、samirem、LucaV)