Reputation: 2800
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.
Upvotes: 3
Views: 1638
Reputation: 1383
Thank you for providing that much of code, it was much easier for me to understand your question.
First, I'll explain your current "User input -> Data grid" flow bellow:
When you are typing, for example, text inside Username:
TextBox
, the text that you are typing is eventually, at some point, backed inside the TextBox.Text
property value, in our case, is the current UserModel.Username
property, because they are binded and he is the property value:
Text="{Binding UserName}"></TextBox>
The fact that they are binded means that no matter when you set UserModel.Username
property, PropertyChanged
is raised and notifies for a change:
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
RaisePropertyChanged("Username"); // notification
}
}
When PropertyChanged
is raises, it notifies the changes for all UserModel.Username
subscribers, in our case, one of the DataGrid.Columns
, is a subscriber.
<DataGridTextColumn Header="Username" IsReadOnly="True" Binding="{Binding Username}" Width="*"></DataGridTextColumn>
The problem with the flow above starts at the place where you back the user input text.
What you need is a place to back your user input text without setting it directly to the current UserModel.Username
property, because if it will, it will start the flow I described above.
I would like the
DataGrid
to only be updated once the user clicks "Save"
My solution for your question is instead of directly backing the TextBox
es texts inside the current UserModel
, backing the texts inside a temporary place, so when you click on "Save", it will copy the text from there to the current UserModel
, and the properties set
accessor inside CopyTo
method will automatically update the DataGrid
.
I made the following changes for your code, the rest left the same:
View
<GroupBox x:Name="GroupBoxDetails" Header="User Details" DataContext="{Binding Path=TemporarySelectedUser, Mode=TwoWay, UpdateSourceTrigger=LostFocus}">
...
<Button Content="Save"
Command="{Binding Path=SaveCommand}"
CommandParameter="{Binding Path=TemporarySelectedUser}"/> // CommandParameter is optional if you'll use SaveCommand with no input members.
ViewModel
...
public UserModel TemporarySelectedUser { get; private set; }
...
TemporarySelectedUser = new UserModel(); // once in the constructor.
...
private UserModel _currentUser;
public UserModel CurrentUser
{
get { return _currentUser; }
set
{
_currentUser = value;
if (value != null)
value.CopyTo(TemporarySelectedUser);
RaisePropertyChanged("CurrentUser");
}
}
...
private ICommand _saveCommand;
public ICommand SaveCommand
{
get
{
return _saveCommand ??
(_saveCommand = new Command<UserModel>(SaveExecute));
}
}
public void SaveExecute(UserModel updatedUser)
{
UserModel oldUser = Users[
Users.IndexOf(
Users.First(value => value.Id == updatedUser.Id))
];
// updatedUser is always TemporarySelectedUser.
updatedUser.CopyTo(oldUser);
}
...
Model
public void CopyTo(UserModel target)
{
if (target == null)
throw new ArgumentNullException();
target.FirstName = this.FirstName;
target.LastName = this.LastName;
target.Username = this.Username;
target.Id = this.Id;
}
User input --text input--> Temporary user --click Save--> Updates user and UI.
Its seems that your MVVM approach is View-First, one of many "View-First" approach guidelines is for each View you should create a corresponding ViewModel. So, it would be more "accurate" to rename your ViewModel after the View its abstracting, e.g. rename UserViewModel
to AdminUserViewModel
.
Also, You can rename your SaveCommand
to Command
because it is answers the whole command pattern solution, rather then the special "saving" case.
I would suggest you to use one of the MVVM frameworks (MVVMLight is my recommendation) as best practice for MVVM study, there are plenty out there.
Hope it helped.
Upvotes: 2