WPF可编辑的主从结构,使用DataGrid在保存时更新

3
我很新于WPF,所以我想从简单的开始:一个窗口,允许用户管理用户。该窗口包含一个DataGrid和几个输入控件,用于添加或编辑用户。当用户在网格中选择记录时,数据绑定到输入控件。然后用户可以进行必要的更改并单击“保存”按钮以保留更改。
然而,发生的情况是,一旦用户在其中一个输入控件中进行更改,DataGrid中对应的数据也会在“保存”按钮被点击之前更新。我希望DataGrid仅在用户单击“保存”后才更新。
以下是视图的XAML代码:
<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>

这个模型本身没有什么特别的(基类仅实现了INotifyPropertyChanged接口并触发相关事件):

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

视图模型(IRemoteStore提供对底层记录存储的访问):
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");
        }
    }
}

为了完整起见,这是我保存ICommand的实现(目前还没有实际持久化任何内容,因为我想先正确地使数据绑定工作):

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

显然,我希望使用纯MVVM模式来实现这个。我已经尝试将DataGrid中的绑定设置为OneWay,但这会导致网格中的更改不会反映出来(尽管新条目确实被添加)。
我还查看了这篇 SO问题,该问题建议在ViewModel上使用“selected”属性。我的原始实现,如上所述,已经有了这样一个属性(称为“CurrentUser”),但是使用当前的绑定配置,网格仍会在用户进行更改时更新。
如果您能提供任何帮助,将不胜感激,因为我已经在解决这个问题上碰壁了几个小时。如果我漏掉了任何内容,请评论并我会更新帖子。谢谢。
1个回答

2
感谢您提供如此多的代码,这让我更容易理解您的问题。
首先,我将解释一下您当前的“用户输入 -> 数据网格”流程:
当您在用户名:文本框中输入文本时,您所输入的文本最终会在某个时间点被存储在TextBox.Text属性值中,在我们的情况下,它是当前的UserModel.Username属性,因为它们被绑定在一起并且它是属性值。
Text="{Binding UserName}"></TextBox>

他们被绑定的事实意味着,无论何时您设置 UserModel.Username 属性,PropertyChanged 都会被触发并通知变化:即使是在何时。
private string _username;
public string Username
{
    get { return _username; }
    set
    {
        _username = value;
        RaisePropertyChanged("Username"); // notification
    }
}

当触发 PropertyChanged 时,它会通知所有订阅了 UserModel.Username 的变化,而在我们的情况下,DataGrid.Columns 中的一个是订阅者。
<DataGridTextColumn Header="Username" IsReadOnly="True" Binding="{Binding Username}" Width="*"></DataGridTextColumn>

上述流程的问题始于您备份用户输入文本的位置。您需要一个地方来备份用户输入文本,而不是直接设置为当前的UserModel.Username属性,因为如果这样做,它将启动上述流程。
引用: 我希望DataGrid只有在用户点击“保存”后才会更新。 对于您的问题,我的解决方案是:不要直接备份当前UserModel中TextBoxes文本,而是将文本备份到临时位置,因此当您单击“保存”时,它将从那里复制文本到当前的UserModel中,并且CopyTo方法中的属性set访问器将自动更新DataGrid。
我对您的代码进行了以下更改,其余部分保持不变:
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);
}
...

模型

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;
}

用户输入--文本输入-->临时用户--单击保存-->更新用户和UI。

看起来你的MVVM方法是View-First,其中一个“View-First”方法的指导原则是为每个视图创建一个相应的ViewModel。因此,将UserViewModel重命名为AdminUserViewModel会更加“准确”。

此外,您可以将SaveCommand重命名为Command,因为它回答了整个命令模式解决方案,而不是特殊的“保存”情况。

我建议您使用MVVM框架之一(MVVMLight是我的推荐),作为MVVM学习的最佳实践,有很多可供选择。

希望对你有所帮助。


1
谢谢您详细的回答,知道为什么会发生某些事情而不仅仅是如何修复它是很好的...特别是当您还在学习时。我已经添加了“interim”属性,现在它正按照我想要的方式工作。我考虑过使用MVVM框架,但由于我仍在忙于学习,所以在学习WPF的基础知识之前,我不想抽象任何东西。一旦我感到对我的知识感到舒适,我将转向使用这样的框架。再次感谢! - Carl Heinrich Hancke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接