MVVM - 为模型视图实现“Is Dirty”功能以保存数据
Posted
技术标签:
【中文标题】MVVM - 为模型视图实现“Is Dirty”功能以保存数据【英文标题】:MVVM - implementing 'IsDirty' functionality to a ModelView in order to save data 【发布时间】:2011-06-01 04:19:42 【问题描述】:作为 WPF 和 MVVM 的新手,我在一些基本功能上苦苦挣扎。
我先解释一下我的目的,然后附上一些示例代码……
我有一个显示用户列表的屏幕,我在右侧使用可编辑的文本框显示所选用户的详细信息。然后我有一个保存按钮,它是 DataBound,但我只想在数据实际更改时显示这个按钮。即 - 我需要检查“脏数据”。
我有一个完整的 MVVM 示例,其中我有一个名为 User 的模型:
namespace Test.Model
class User
public string UserName get; set;
public string Surname get; set;
public string Firstname get; set;
然后,ViewModel 看起来像这样:
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;
namespace Test.ViewModel
class UserViewModel : ViewModelBase
//Private variables
private ObservableCollection<User> _users;
RelayCommand _userSave;
//Properties
public ObservableCollection<User> User
get
if (_users == null)
_users = new ObservableCollection<User>();
//I assume I need this Handler, but I am stuggling to implement it successfully
//_users.CollectionChanged += HandleChange;
//Populate with users
_users.Add(new User UserName = "Bob", Firstname="Bob", Surname="Smith");
_users.Add(new User UserName = "Smob", Firstname="John", Surname="Davy");
return _users;
//Not sure what to do with this?!?!
//private void HandleChange(object sender, NotifyCollectionChangedEventArgs e)
//
// if (e.Action == NotifyCollectionChangedAction.Remove)
//
// foreach (TestViewModel item in e.NewItems)
//
// //Removed items
//
//
// else if (e.Action == NotifyCollectionChangedAction.Add)
//
// foreach (TestViewModel item in e.NewItems)
//
// //Added items
//
//
//
//Commands
public ICommand UserSave
get
if (_userSave == null)
_userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
return _userSave;
void UserSaveExecute()
//Here I will call my DataAccess to actually save the data
bool UserSaveCanExecute
get
//This is where I would like to know whether the currently selected item has been edited and is thus "dirty"
return false;
//constructor
public UserViewModel()
“RelayCommand”只是一个简单的包装类,“ViewModelBase”也是如此。 (为了清楚起见,我会附上后者)
using System;
using System.ComponentModel;
namespace Test.ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
protected ViewModelBase()
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
public void Dispose()
this.OnDispose();
protected virtual void OnDispose()
最后 - XAML
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Test.ViewModel"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:UserViewModel/>
</Window.DataContext>
<Grid>
<ListBox Height="238" HorizontalAlignment="Left" Margin="12,12,0,0" Name="listBox1" VerticalAlignment="Top"
Width="197" ItemsSource="Binding Path=User" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="Binding Path=Firstname"/>
<TextBlock Text="Binding Path=Surname"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="232,16,0,0" Name="label1" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,21,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="Binding Path=User/UserName" />
<Label Content="Surname" Height="28" HorizontalAlignment="Left" Margin="232,50,0,0" Name="label2" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,52,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="Binding Path=User/Surname" />
<Label Content="Firstname" Height="28" HorizontalAlignment="Left" Margin="232,84,0,0" Name="label3" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,86,0,0" Name="textBox3" VerticalAlignment="Top" Width="120" Text="Binding Path=User/Firstname" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="368,159,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="Binding Path=UserSave" />
</Grid>
</Window>
所以基本上,当我编辑姓氏时,应该启用保存按钮;如果我撤消我的编辑 - 那么它应该再次禁用,因为没有任何改变。
我在很多例子中都看到了这一点,但还没有弄清楚如何去做。
任何帮助将不胜感激! 布伦丹
【问题讨论】:
【参考方案1】:根据我的经验,如果您在视图模型中实现IsDirty
,您可能还希望视图模型实现IEditableObject
。
假设您的视图模型是通常的类型,实现PropertyChanged
和一个私有或受保护的OnPropertyChanged
方法来提升它,设置IsDirty
很简单:你只需在OnPropertyChanged
中设置IsDirty
,如果它还不是真的。
您的IsDirty
设置器应该,如果属性是假的,现在是真的,调用BeginEdit
。
您的Save
命令应该调用EndEdit
,这会更新数据模型并将IsDirty
设置为false。
您的Cancel
命令应该调用CancelEdit
,它会从数据模型中刷新视图模型并将IsDirty
设置为false。
CanSave
和 CanCancel
属性(假设您对这些命令使用 RelayCommand
)只返回 IsDirty
的当前值。
请注意,由于这些功能都不依赖于视图模型的具体实现,您可以将其放在抽象基类中。派生类不必实现任何与命令相关的属性或IsDirty
属性;他们只需要覆盖BeginEdit
、EndEdit
和CancelEdit
。
【讨论】:
【参考方案2】:我已经为包装在我的 ViewModel 中的模型实现 IsDirty 做了一些工作。
结果确实简化了我的 ViewModel:
public class PersonViewModel : ViewModelBase
private readonly ModelDataStore<Person> data;
public PersonViewModel()
data = new ModelDataStore<Person>(new Person());
public PersonViewModel(Person person)
data = new ModelDataStore<Person>(person);
#region Properties
#region Name
public string Name
get return data.Model.Name;
set data.SetPropertyAndRaisePropertyChanged("Name", value, this);
#endregion
#region Age
public int Age
get return data.Model.Age;
set data.SetPropertyAndRaisePropertyChanged("Age", value, this);
#endregion
#endregion
代码@http://wpfcontrols.codeplex.com/ 查看 Patterns 程序集和 MVVM 文件夹下,您会找到一个 ModelDataStore 类。
附: 我还没有对其进行全面测试,只是进行了非常简单的测试,您会发现 Test 程序集。
【讨论】:
【参考方案3】:我建议您使用GalaSoft MVVM Light Toolkit,因为它比 DIY 方法更容易实现。
对于脏读,你需要保留每个字段的快照,并从UserSaveCanExecute()
方法返回true或false,这将相应地启用/禁用命令按钮。
【讨论】:
我也会使用 MVVM Light Toolkit 谢谢,我已经安装了 MVVM-Light 工具包,但我还没有看到任何轻松实现“IsDirty”功能的方法。然而,我设法解决了我的问题(也许不是最好的方法 - 但它有效) - 稍后将详细回答我自己的问题 MVVM 不支持脏读功能。这只是一个建议,以尽量减少实现 MVVM 模式的工作量。很高兴知道您已经解决了脏读问题。现在我建议在轻量级工具包中探索进一步的消息传递和事件命令,这将为您提供更多控制权。玩得开心。 投票赞成建议 another_framework (tm) 但实际上并没有回答这个问题?普里莫!【参考方案4】:如果您想采用框架方法而不是自己编写基础架构,您可以使用 CSLA (http://www.lhotka.net/cslanet/) - Rocky 用于开发业务对象的框架。在属性更改时为您管理对象状态,并且代码库还包括一个示例 ViewModel 类型,该类型支持基础模型、Save 动词和 CanSave 属性。即使您不想使用框架,您也可以从代码中获得灵感。
【讨论】:
【参考方案5】:我想出了一个可行的解决方案。这当然可能不是最好的方法,但我相信随着我了解更多,我可以继续努力......
当我运行项目时,如果我取消任何项目,列表框将被禁用,并且保存按钮启用。如果我撤消我的编辑,则列表框会再次启用,而保存按钮会被禁用。
我已经更改了我的用户模型以实现 INotifyPropertyChanged,并且我还创建了一组私有变量来存储“原始值”和一些逻辑来检查“IsDirty”
using System.ComponentModel;
namespace Test.Model
public class User : INotifyPropertyChanged
//Private variables
private string _username;
private string _surname;
private string _firstname;
//Private - original holders
private string _username_Orig;
private string _surname_Orig;
private string _firstname_Orig;
private bool _isDirty;
//Properties
public string UserName
get
return _username;
set
if (_username_Orig == null)
_username_Orig = value;
_username = value;
SetDirty();
public string Surname
get return _surname;
set
if (_surname_Orig == null)
_surname_Orig = value;
_surname = value;
SetDirty();
public string Firstname
get return _firstname;
set
if (_firstname_Orig == null)
_firstname_Orig = value;
_firstname = value;
SetDirty();
public bool IsDirty
get
return _isDirty;
public void SetToClean()
_username_Orig = _username;
_surname_Orig = _surname;
_firstname_Orig = _firstname;
_isDirty = false;
OnPropertyChanged("IsDirty");
private void SetDirty()
if (_username == _username_Orig && _surname == _surname_Orig && _firstname == _firstname_Orig)
if (_isDirty)
_isDirty = false;
OnPropertyChanged("IsDirty");
else
if (!_isDirty)
_isDirty = true;
OnPropertyChanged("IsDirty");
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
然后,我的 ViewModel 也发生了一些变化......
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;
using System.ComponentModel;
namespace Test.ViewModel
class UserViewModel : ViewModelBase
//Private variables
private ObservableCollection<User> _users;
RelayCommand _userSave;
private User _selectedUser = new User();
//Properties
public ObservableCollection<User> User
get
if (_users == null)
_users = new ObservableCollection<User>();
_users.CollectionChanged += (s, e) =>
if (e.Action == NotifyCollectionChangedAction.Add)
// handle property changing
foreach (User item in e.NewItems)
((INotifyPropertyChanged)item).PropertyChanged += (s1, e1) =>
OnPropertyChanged("EnableListBox");
;
;
//Populate with users
_users.Add(new User UserName = "Bob", Firstname="Bob", Surname="Smith");
_users.Add(new User UserName = "Smob", Firstname="John", Surname="Davy");
return _users;
public User SelectedUser
get return _selectedUser;
set _selectedUser = value;
public bool EnableListBox
get return !_selectedUser.IsDirty;
//Commands
public ICommand UserSave
get
if (_userSave == null)
_userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
return _userSave;
void UserSaveExecute()
//Here I will call my DataAccess to actually save the data
//Save code...
_selectedUser.SetToClean();
OnPropertyChanged("EnableListBox");
bool UserSaveCanExecute
get
return _selectedUser.IsDirty;
//constructor
public UserViewModel()
最后,XAML
我更改了用户名、姓氏和名字的绑定以包括 UpdateSourceTrigger=PropertyChanged
然后我绑定了列表框的 SelectedItem 和 IsEnabled
正如我在开始时所说的那样 - 这可能不是最好的解决方案,但它似乎有效......
【讨论】:
【参考方案6】:由于您的 UserSave 命令位于 ViewModel 中,我会在那里跟踪“脏”状态。我会将数据绑定到 ListBox 中的选定项目,当它发生变化时,存储选定用户属性的当前值的快照。然后您可以与此进行比较以确定是否应启用/禁用该命令。
但是,由于您直接绑定到模型,因此您需要一些方法来确定是否发生了变化。您也可以在模型中实现 INotifyPropertyChanged,或者将属性包装在 ViewModel 中。
请注意,当命令的 CanExecute 发生变化时,您可能需要触发 CommandManager.InvalidateRequerySuggested()。
【讨论】:
【参考方案7】:这就是我实现 IsDirty 的方式。在 ViewModal 中为 User 类的每个属性创建一个包装器(使用 IPropertyChanged 继承 User 类并在 User 类中实现 onpropertychanged 无济于事)。您需要将绑定从 UserName 更改为 WrapUserName。
public string WrapUserName
get
return User.UserName
set
User.UserName = value;
OnPropertyChanged("WrapUserName");
现在有一个属性
public bool isPageDirty
get;
set;
由于您的 viewmodal 继承自 baseviewmodal 并且 baseviewmodal 实现了 onPropertyChanged。
UserViewModel.PropertyChanged += (s, e) => isPageDirty = true; ;
如果任何属性更改,isPageDirty 将为 true,因此在保存时检查 isPageDirty。
【讨论】:
谢谢。这看起来很有趣——今晚会玩。然而,我想出了另一个解决方案,稍后我将在下面解释以上是关于MVVM - 为模型视图实现“Is Dirty”功能以保存数据的主要内容,如果未能解决你的问题,请参考以下文章
DevOps is dirty work - What's the deal