刚接触WPF& MVVM我在努力学习一些基本功能。
首先让我解释一下我的目标,然后附上一些示例代码......
我有一个显示用户列表的屏幕,我在右侧显示所选用户的详细信息,其中包含可编辑的文本框。然后我有一个Save按钮,它是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>
所以基本上,当我编辑姓氏时,应该启用“保存”按钮;如果我撤消我的编辑 - 那么它应该再次被禁用,因为没有任何改变。
我在很多例子中都看过这个,但还没有发现如何做到这一点。
任何帮助将不胜感激! 布伦丹
答案 0 :(得分:8)
根据我的经验,如果在视图模型中实现IsDirty
,您可能还希望视图模型实现IEditableObject
。
假设您的视图模型是通常的排序方式,实现PropertyChanged
以及提升它的私有或受保护OnPropertyChanged
方法,设置IsDirty
非常简单:您只需设置{{1如果它不是真的那么在IsDirty
中。
如果属性为false且现在为true,则OnPropertyChanged
setter应该调用IsDirty
。
您的BeginEdit
命令应调用Save
,这会更新数据模型并将EndEdit
设置为false。
您的IsDirty
命令应调用Cancel
,从数据模型中刷新视图模型并将CancelEdit
设置为false。
IsDirty
和CanSave
属性(假设您对这些命令使用CanCancel
)只返回RelayCommand
的当前值。
请注意,由于此功能都不依赖于视图模型的特定实现,因此可以将其放在抽象基类中。派生类不必实现任何与命令相关的属性或IsDirty
属性;他们只需要覆盖IsDirty
,BeginEdit
和EndEdit
。
答案 1 :(得分:4)
我建议您使用GalaSoft MVVM Light Toolkit,因为它比DIY方法更容易实现。
对于脏读,您需要保留每个字段的快照,并从UserSaveCanExecute()
方法返回true或false,这将相应地启用/禁用命令按钮。
答案 2 :(得分:4)
我已经为我的ViewModel中包含的模型实现了IsDirty的一些工作。
结果真正简化了我的ViewModels:
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类。
P.S。 我没有对它进行过全面的测试,只是你会找到测试组件的非常简单的测试。
答案 3 :(得分:3)
如果您想采用框架方法而不是自己编写基础架构,可以使用CSLA(http://www.lhotka.net/cslanet/) - Rocky的框架来开发业务对象。在属性更改时为您管理对象状态,代码库还包括支持底层模型,Save动词和CanSave属性的示例ViewModel类型。您可以从代码中获取灵感,即使您不想使用该框架。
答案 4 :(得分:2)
我想出了一个有效的解决方案。这当然不是最好的方式,但我相信我可以继续努力,因为我了解更多......
当我运行项目时,如果我插入任何项目,则禁用列表框,并启用保存按钮。如果我撤消编辑,则会再次启用列表框,并禁用保存按钮。
我已经更改了我的用户模型以实现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
正如我在开始时所说的那样 - 它可能不是最佳解决方案,但似乎有效......
答案 5 :(得分:0)
由于您的UserSave命令在ViewModel中,我会在那里跟踪“脏”状态。我将数据绑定到ListBox中的选定项目,当它更改时,存储所选用户属性的当前值的快照。然后,您可以与此进行比较,以确定是否应启用/禁用该命令。
但是,由于您直接绑定到模型,因此您需要一些方法来确定是否有更改。您还可以在模型中实现INotifyPropertyChanged,或者将属性包装在ViewModel中。
请注意,当命令的CanExecute更改时,您可能需要触发CommandManager.InvalidateRequerySuggested()。
答案 6 :(得分:0)
这就是我实施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。