可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Being new to WPF & MVVM I struggling with some basic functionality.
Let me first explain what I am after, and then attach some example code...
I have a screen showing a list of users, and I display the details of the selected user on the right-hand side with editable textboxes. I then have a Save button which is DataBound, but I would only like this button to display when data has actually changed. ie - I need to check for "dirty data".
I have a fully MVVM example in which I have a Model called User:
namespace Test.Model
{
class User
{
public string UserName { get; set; }
public string Surname { get; set; }
public string Firstname { get; set; }
}
}
Then, the ViewModel looks like this:
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()
{
}
}
}
The "RelayCommand" is just a simple wrapper class, as is the "ViewModelBase". (I'll attach the latter though just for clarity)
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()
{
}
}
}
Finally - the 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>
So basically, when I edit a surname, the Save button should be enabled; and if I undo my edit - well then it should be Disabled again as nothing has changed.
I have seen this in many examples, but have not yet found out how to do it.
Any help would be much appreciated!
Brendan
回答1:
In my experience, if you implement IsDirty
in your view model, you probably also want the view model to implement IEditableObject
.
Assuming that your view model is the usual sort, implementing PropertyChanged
and a private or protected OnPropertyChanged
method that raises it, setting IsDirty
is simple enough: you just set IsDirty
in OnPropertyChanged
if it isn't already true.
Your IsDirty
setter should, if the property was false and is now true, call BeginEdit
.
Your Save
command should call EndEdit
, which updates the data model and sets IsDirty
to false.
Your Cancel
command should call CancelEdit
, which refreshes the view model from the data model and sets IsDirty
to false.
The CanSave
and CanCancel
properties (assuming you're using a RelayCommand
for these commands) just return the current value of IsDirty
.
Note that since none of this functionality depends on the specific implementation of the view model, you can put it in an abstract base class. Derived classes don't have to implement any of the command-related properties or the IsDirty
property; they just have to override BeginEdit
, EndEdit
, and CancelEdit
.
回答2:
I've done some work on implementing IsDirty for models that is wrapped in my ViewModel.
The result really simplified my 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
}
Code @ http://wpfcontrols.codeplex.com/
Check under the Patterns assembly and MVVM folder, you'll find a ModelDataStore class.
P.S.
I haven't done a full scale test on it, just the really simple test you'll find the Test assembly.
回答3:
I would suggest you to use GalaSoft MVVM Light Toolkit as it is much more easier to implement than DIY approach.
For dirty reads, you need to keep the snapshot of each fields, and return true or false from UserSaveCanExecute()
method, which will enable / disable command button accordingly.
回答4:
If you wanted to take a framework approach rather than writing the infrastructure yourself, you could use CSLA (http://www.lhotka.net/cslanet/) - Rocky's framework for developing business objects. Object state is managed for you on property changes, and the code base also includes an example ViewModel type which supports an underlying model, a Save verb, and a CanSave property. You may be able to take inspiration from the code, even you didn't want to use the framework.
回答5:
I have come up with a working solution. This may of course not be the best way, but I am sure I can work on it as I learn more...
When I run the project, if I cange any item, the list box is disabled, and the save button enabled. If I undo my edits, then the list box is enabled again, and the save button disabled.
I have changed my User Model to implement INotifyPropertyChanged, and I have also created a set of private variables to store the "original values" and some logic to check for "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));
}
}
}
Then, my ViewModel has changed a bit too....
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()
{
}
}
Finally, the XAML
I changed the bindings on the Username, Surname & Firstname to include UpdateSourceTrigger=PropertyChanged
And then I bound the listbox's SelectedItem and IsEnabled
As I said in the beginning - it may not be the best solution, but it seems to work...
回答6:
Since your UserSave command is in the ViewModel, I would do the tracking of the "dirty" state there. I would databind to the selected item in the ListBox, and when it changes, store a snapshot of the current values of the selected user's properties. Then you can compare to this to determine if the command should be enabled/disabled.
However, since you are binding directly to the model, you need some way to find out if something changed. Either you also implement INotifyPropertyChanged in the model, or wrap the properties in a ViewModel.
Note that when the CanExecute of the command changes, you may need to fire CommandManager.InvalidateRequerySuggested().
回答7:
This is how I have implemented IsDirty. Create a wrapper for every property of User class (inheriting User class with IPropertyChanged and implementing onpropertychanged in User class wont help) in your ViewModal. You need to change your binding from UserName to WrapUserName.
public string WrapUserName
{
get
{
return User.UserName
}
set
{
User.UserName = value;
OnPropertyChanged("WrapUserName");
}
}
Now have a property
public bool isPageDirty
{
get;
set;
}
Since your viewmodal inherits from baseviewmodal and baseviewmodal implements onPropertyChanged.
UserViewModel.PropertyChanged += (s, e) => { isPageDirty = true; };
In case any of the propertychanges,isPageDirty will be true, So while saving you chan check isPageDirty.