I'm very new to WPF, so I thought I'd start with something simple: A window that allows users to manage users. The Window contains a DataGrid
along with several input controls to add or edit users. When a user selects a record in the grid, the data is bound to the input controls. The user can then make the required changes & click the "Save" button to persist the changes.
What's happening however, is that as soon as a user makes a change in one of the input controls, the corresponding data in the DataGrid
gets updated as well, before the "Save" button was clicked. I would like the DataGrid
to only be updated once the user clicks "Save".
Here is the XAML for the view:
<Window x:Class="LearnWPF.Views.AdminUser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vms="clr-namespace:LearnWPF.ViewModels"
Title="User Administration" Height="400" Width="450"
ResizeMode="NoResize">
<Window.DataContext>
<vms:UserViewModel />
</Window.DataContext>
<StackPanel>
<GroupBox x:Name="grpDetails" Header="User Details" DataContext="{Binding CurrentUser, Mode=OneWay}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0">First Name:</Label>
<TextBox Grid.Column="1" Grid.Row="0" Style="{StaticResource TextBox}" Text="{Binding FirstName}"></TextBox>
<Label Grid.Column="0" Grid.Row="1">Surname:</Label>
<TextBox Grid.Column="1" Grid.Row="1" Style="{StaticResource TextBox}" Text="{Binding LastName}"></TextBox>
<Label Grid.Column="0" Grid.Row="2">Username:</Label>
<TextBox Grid.Column="1" Grid.Row="2" Style="{StaticResource TextBox}" Text="{Binding Username}"></TextBox>
<Label Grid.Column="0" Grid.Row="3">Password:</Label>
<PasswordBox Grid.Column="1" Grid.Row="3" Style="{StaticResource PasswordBox}"></PasswordBox>
<Label Grid.Column="0" Grid.Row="4">Confirm Password:</Label>
<PasswordBox Grid.Column="1" Grid.Row="4" Style="{StaticResource PasswordBox}"></PasswordBox>
</Grid>
</GroupBox>
<StackPanel Orientation="Horizontal">
<Button Style="{StaticResource Button}" Command="{Binding SaveCommand}" CommandParameter="{Binding CurrentUser}">Save</Button>
<Button Style="{StaticResource Button}">Cancel</Button>
</StackPanel>
<DataGrid x:Name="grdUsers" AutoGenerateColumns="False" CanUserAddRows="False" CanUserResizeRows="False"
Style="{StaticResource DataGrid}" ItemsSource="{Binding Users}" SelectedItem="{Binding CurrentUser, Mode=OneWayToSource}">
<DataGrid.Columns>
<DataGridTextColumn Header="Full Name" IsReadOnly="True" Binding="{Binding FullName}" Width="2*"></DataGridTextColumn>
<DataGridTextColumn Header="Username" IsReadOnly="True" Binding="{Binding Username}" Width="*"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Window>
The Model has nothing special in it (the base class merely implements the INotifyPropertyChanged
interface & firing the associated event):
public class UserModel : PropertyChangedBase
{
private int _id;
public int Id
{
get { return _id; }
set
{
_id = value;
RaisePropertyChanged("Id");
}
}
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
private string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
RaisePropertyChanged("Username");
}
}
public string FullName
{
get { return String.Format("{0} {1}", FirstName, LastName); }
}
}
The ViewModel (IRemoteStore
provides access to the underlying record store):
public class UserViewModel : PropertyChangedBase
{
private IRemoteStore _remoteStore = Bootstrapper.RemoteDataStore;
private ICommand _saveCmd;
public UserViewModel()
{
Users = new ObservableCollection<UserModel>();
foreach (var user in _remoteStore.GetUsers()) {
Users.Add(user);
}
_saveCmd = new SaveCommand<UserModel>((model) => {
Users[Users.IndexOf(Users.First(e => e.Id == model.Id))] = model;
});
}
public ICommand SaveCommand
{
get { return _saveCmd; }
}
public ObservableCollection<UserModel> Users { get; set; }
private UserModel _currentUser;
public UserModel CurrentUser
{
get { return _currentUser; }
set
{
_currentUser = value;
RaisePropertyChanged("CurrentUser");
}
}
}
And for the sake of completeness, here's the implementation of my Save ICommand
(this doesn't actually persist anything yet, as I wanted to get the databinding working correctly first):
public class SaveCommand<T> : ICommand
{
private readonly Action<T> _saved;
public SaveCommand(Action<T> saved)
{
_saved = saved;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_saved((T)parameter);
}
}
As is apparent, I'm looking to implement this using a pure MVVM pattern. I've tried setting the bindings in the DataGrid
to OneWay
, but this causes changes to not be reflected in the grid (although new entries do get added).
I've also had a look at this SO question, which suggested using a "selected" property on the ViewModel. My original implementation, as posted above, already had such a property (called "CurrentUser"), but with the current binding configuration, the grid is still updated as users make changes.
Any assistance would be greatly appreciated, as I've been bumping my head against this issue for several hours now. If I've left anything out, please comment & I will update the post. Thank you.