WPF可编辑的主-细节与数据网格更新保存
本文关键字:数据网 数据 网格 更新 保存 细节 编辑 WPF | 更新日期: 2023-09-27 18:02:08
我对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); }
}
}
ViewModel (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");
}
}
}
为了完整起见,这里是我的Save 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"),但是在当前的绑定配置中,网格仍然会随着用户的更改而更新。
任何帮助都将是非常感激的,因为我已经为这个问题绞尽脑汁好几个小时了。如果我遗漏了什么,请评论&我会更新帖子的。谢谢你。感谢您提供这么多代码,这对我来说更容易理解您的问题。
首先,我将解释当前的"用户输入->数据网格"流程如下:
例如,当你输入Username:
TextBox
中的文本时,你输入的文本最终会在某个时候被备份到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
内的 TextBox
es文本,而是支持临时位置内的文本,所以当你点击"保存"时,它会将文本从那里复制到当前UserModel
,并且CopyTo
方法内的属性set
访问器将自动更新DataGrid
。
我对你的代码做了以下更改,其余部分保持不变:
<<p> 视图/strong><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方法是视图优先,许多"视图优先"方法指南之一是为每个视图创建一个相应的ViewModel。因此,在视图抽象之后重新命名ViewModel会更"准确",例如将UserViewModel
重命名为AdminUserViewModel
。
另外,您可以将SaveCommand
重命名为Command
,因为它回答了整个命令模式解决方案,而不是特殊的"保存"情况。
我建议你使用其中一个MVVM框架(MVVMLight是我的推荐)作为MVVM研究的最佳实践,那里有很多。
希望有帮助。