WPF editable master part with DataGrid refresh on save

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 also contains DataGrid

several input controls for adding or editing users. When the user selects an entry in the grid, the data is bound to the input elements. The user can then make the necessary changes and click the "Save" button to save the changes.

What happens, however, is that as soon as the user makes changes to one of the input controls, the corresponding data is DataGrid

also updated before clicking the Save button. I would like it to be DataGrid

updated only after the user clicks Save.

Here's 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>

      

There is nothing special about the model (the base class just implements the interface INotifyPropertyChanged

and fires 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); }
    }
}

      

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 completeness, here's the implementation of my Save ICommand

(this hasn't actually survived yet, as I wanted to handle the data binding 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 obvious, I want to implement this using a pure MVVM pattern. I tried setting the bindings to DataGrid

on OneWay

, but that makes the changes not reflect in the grid (although new entries are added).

I also looked at this SO question which suggested using the "selected" property in the ViewModel. My original implementation, as stated above, already had such a property (called "CurrentUser"), but with the current binding configuration, the grid is still updated as users make changes.

Any help would be greatly appreciated as I have been bumping into this issue for hours now. If I haven't left anything, please comment and I'll update the post. Thank.

+3


source to share


1 answer


Thanks for providing most of the code, it was much easier for me to understand your question.

First, I will explain your current "User Login -> Data Table" below:

When you type, for example, text inside Username:

TextBox

, the text that you enter is eventually, at some point, maintained inside the property value TextBox.Text

, in our case it is the current UserModel.Username

one because they are bound and it is the property value:

Text="{Binding UserName}"></TextBox>

      

The fact that they are bound means that no matter when you set the property UserModel.Username

, it PropertyChanged

hoists up and notifies the change:

private string _username;
public string Username
{
    get { return _username; }
    set
    {
        _username = value;
        RaisePropertyChanged("Username"); // notification
    }
}

      

When PropertyChanged

promoted, it notifies all subscribers of the change UserModel.Username

, in our case one of DataGrid.Columns

the subscribers is.

<DataGridTextColumn Header="Username" IsReadOnly="True" Binding="{Binding Username}" Width="*"></DataGridTextColumn>

      

The problem with the flow above starts at the place where you return the user input text. You will need a place to enter your user's text without setting it directly to the current property UserModel.Username

, because if there is, it will start the flow described above.

I would like to be DataGrid

updated only after the user clicks "Save"

My solution for your question is instead of directly reinforcing the TextBox

es texts inside the current UserModel

, backing the texts in a temporary location, so when you click Save, it copies the text from there to the current one UserModel

, and the set

property method set

within the method CopyTo

will automatically update DataGrid

.

I made the following changes to your code, the rest are 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 - Input Text -> Temporary User --click Save -> User and UI Updates.

Your MVVM approach seems to be View-First, one of the many principles of the "View-First" approach for each view is to create a corresponding ViewModel. Thus, it would be more "precise" to rename your ViewModel after seeing it abstracted for example. rename UserViewModel

to AdminUserViewModel

.

Alternatively, you can rename yours SaveCommand

to Command

, because it responds to the entire team template solution, not a special "save" case.

I would suggest that you use one of the MVVM frameworks (MVVMLight is my recommendation) as a best practice for exploring MVVM, there are many.

Hope it helped.

+2


source







All Articles