getter/setter 是常规 C# 属性的一个特性。它不是 WPF 独有的。
这个单向/双向的东西是关于 WPF 数据绑定,它不需要你创建依赖属性 - 只是为了使用它们。
依赖属性内置在控件本身中。它们允许您在将控件实例添加到表单时直接引用这些属性。它们让您的自定义控件感觉更“原生”。
通常它们用于实现可以使用数据绑定的属性。在您的应用程序中,您大多只是使用数据绑定,而不是为它实现新的钩子。
...如果有人单击添加或编辑按钮,所有数据输入字段都将变为“启用”,新记录的空白数据或编辑现有数据。同时,添加/编辑按钮将被禁用,但保存/取消按钮现在将被启用。
同样,当通过保存/取消完成时,它会禁用所有输入字段、保存/取消并重新启用添加/编辑按钮。
我会完成你想要完成的事情:
- 视图模型
- 视图上的数据绑定到该视图模型
- 在该视图模型上公开 ICommand(用于按钮)
- 视图模型上的 INotifyPropertyChanged(适用于所有属性)
不需要为此场景创建新的依赖属性。您只需使用现有的进行数据绑定。
这是使用数据绑定和 MVVM 样式进行 WPF 的代码示例/教程。
设置项目
我在新建项目向导中创建了一个 WPF 应用程序,并将其命名为 MyProject。
我设置了我的项目名称和命名空间以匹配普遍接受的事物方案。您应该在解决方案资源管理器 -> 项目 -> 右键单击 -> 属性中设置这些属性。
我还有一个我喜欢用于 WPF 项目的自定义文件夹方案:
出于组织目的,我将视图保存在其自己的“视图”文件夹中。这也反映在命名空间中,因为您的命名空间应该与您的文件夹匹配 (namespace MyCompany.MyProject.View)。
我还编辑了 AssemblyInfo.cs,并清理了我的程序集引用和应用程序配置,但这只是一些乏味的事情,我将留给读者作为练习:)
创建视图
从设计师开始,让一切看起来都很好。不要在后面添加任何代码,或者做任何其他工作。只需在设计器中玩耍,直到看起来正确(尤其是在调整大小时)。这是我最终得到的结果:
查看/EntryView.xaml:
<Window x:Class="MyCompany.MyProject.View.EntryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Entry View" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Text="Test 1" Grid.Row="0" />
<TextBox Text="Test 2" Grid.Row="1" Margin="0,6,0,0" />
<TextBox Text="Test 3" Grid.Row="2" Margin="0,6,0,0" />
<TextBox Text="Test 4" Grid.Row="3" Margin="0,6,0,0" />
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Content="Edit" IsEnabled="True" Grid.Column="0"
HorizontalAlignment="Left" Width="75" />
<Button Content="Save" IsEnabled="False" Grid.Column="1"
Width="75" />
<Button Content="Cancel" IsEnabled="False" Grid.Column="2"
Width="75" Margin="6,0,0,0" />
</Grid>
</Grid>
</Window>
查看/EntryView.xaml.cs:
using System.Windows;
namespace MyCompany.MyProject.View
{
public partial class EntryView : Window
{
public EntryView()
{
InitializeComponent();
}
}
}
我没有在这些控件上创建任何Name 属性。那是故意的。我将使用 MVVM,并且不会使用任何后面的代码。我会让设计师做它想做的事,但我不会碰任何代码。
创建视图模型
接下来我将制作我的视图模型。这应该以它为视图服务的方式设计,但理想情况下可以独立于视图。我不会担心太多,但重点是您不必必须拥有一对一的视图控件和视图模型对象。
我试图让我的视图/视图模型在更大的应用程序上下文中有意义,所以我将在这里开始使用视图模型。我们将把这个“可编辑的表单”变成一个 rolodex 条目。
我们将创建一个我们首先需要的辅助类...
ViewModel/DelegateCommand.cs:
using System;
using System.Windows.Input;
namespace MyCompany.MyProject.ViewModel
{
public class DelegateCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public DelegateCommand(Action execute)
: this(execute, CanAlwaysExecute)
{
}
public DelegateCommand(Action execute, Func<bool> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
if (canExecute == null)
throw new ArgumentNullException("canExecute");
_execute = o => execute();
_canExecute = o => canExecute();
}
public bool CanExecute(object parameter)
{
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, new EventArgs());
}
private static bool CanAlwaysExecute()
{
return true;
}
}
}
ViewModel/EntryViewModel.cs:
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace MyCompany.MyProject.ViewModel
{
public class EntryViewModel : INotifyPropertyChanged
{
private readonly string _initialName;
private readonly string _initialEmail;
private readonly string _initialPhoneNumber;
private readonly string _initialRelationship;
private string _name;
private string _email;
private string _phoneNumber;
private string _relationship;
private bool _isInEditMode;
private readonly DelegateCommand _makeEditableOrRevertCommand;
private readonly DelegateCommand _saveCommand;
private readonly DelegateCommand _cancelCommand;
public EntryViewModel(string initialNamename, string email,
string phoneNumber, string relationship)
{
_isInEditMode = false;
_name = _initialName = initialNamename;
_email = _initialEmail = email;
_phoneNumber = _initialPhoneNumber = phoneNumber;
_relationship = _initialRelationship = relationship;
MakeEditableOrRevertCommand = _makeEditableOrRevertCommand =
new DelegateCommand(MakeEditableOrRevert, CanEditOrRevert);
SaveCommand = _saveCommand =
new DelegateCommand(Save, CanSave);
CancelCommand = _cancelCommand =
new DelegateCommand(Cancel, CanCancel);
}
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
public string Email
{
get { return _email; }
set
{
_email = value;
RaisePropertyChanged("Email");
}
}
public string PhoneNumber
{
get { return _phoneNumber; }
set
{
_phoneNumber = value;
RaisePropertyChanged("PhoneNumber");
}
}
public string Relationship
{
get { return _relationship; }
set
{
_relationship = value;
RaisePropertyChanged("Relationship");
}
}
public bool IsInEditMode
{
get { return _isInEditMode; }
private set
{
_isInEditMode = value;
RaisePropertyChanged("IsInEditMode");
RaisePropertyChanged("CurrentEditModeName");
_makeEditableOrRevertCommand.RaiseCanExecuteChanged();
_saveCommand.RaiseCanExecuteChanged();
_cancelCommand.RaiseCanExecuteChanged();
}
}
public string CurrentEditModeName
{
get { return IsInEditMode ? "Revert" : "Edit"; }
}
public ICommand MakeEditableOrRevertCommand { get; private set; }
public ICommand SaveCommand { get; private set; }
public ICommand CancelCommand { get; private set; }
private void MakeEditableOrRevert()
{
if (IsInEditMode)
{
// Revert
Name = _initialName;
Email = _initialEmail;
PhoneNumber = _initialPhoneNumber;
Relationship = _initialRelationship;
}
IsInEditMode = !IsInEditMode; // Toggle the setting
}
private bool CanEditOrRevert()
{
return true;
}
private void Save()
{
AssertEditMode(isInEditMode: true);
IsInEditMode = false;
// Todo: Save to file here, and trigger close...
}
private bool CanSave()
{
return IsInEditMode;
}
private void Cancel()
{
AssertEditMode(isInEditMode: true);
IsInEditMode = false;
// Todo: Trigger close form...
}
private bool CanCancel()
{
return IsInEditMode;
}
private void AssertEditMode(bool isInEditMode)
{
if (isInEditMode != IsInEditMode)
throw new InvalidOperationException();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged Members
}
}
与这种类型的工作流程一样,我在最初创建视图时遗漏了一些要求。例如,我发现有一个“还原”功能可以撤消更改,但保持对话框打开是有意义的。我还发现我可以为此目的重复使用编辑按钮。所以我创建了一个属性,我将读取它来获取编辑按钮的名称。
视图模型包含很多代码来做一些简单的事情,但大部分都是用于连接属性的样板。不过,这个样板文件给了你一些力量。它有助于将您与您的视图隔离开来,因此您的视图可以在视图模型没有更改或仅进行微小更改的情况下发生巨大变化。
如果视图模型变得太大,您可以开始将其推入额外的子视图模型。在最有意义的地方创建它们,并将它们作为此视图模型上的属性返回。 WPF 数据绑定机制支持链接数据上下文。稍后我们联系起来时,您会发现有关此数据上下文的信息。
将视图连接到我们的视图模型
要将视图连接到视图模型,您必须在视图上设置DataContext 属性以指向您的视图模型。
有些人喜欢在 XAML 代码中实例化和指定视图模型。虽然这可以工作,但我喜欢保持视图和视图模型彼此独立,所以我确保使用一些第三类来连接两者。
通常我会使用依赖注入容器来连接我的所有代码,这需要大量工作,但要让所有部分保持独立。但是对于这么简单的应用程序,我喜欢使用 App 类将我的东西绑定在一起。让我们去编辑它:
App.xaml:
<Application x:Class="MyCompany.MyProject.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="ApplicationStartup">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.cs:
using System.Windows;
namespace MyCompany.MyProject
{
public partial class App : Application
{
private void ApplicationStartup(object sender, StartupEventArgs e)
{
// Todo: Somehow load initial data...
var viewModel = new ViewModel.EntryViewModel(
"some name", "some email", "some phone number",
"some relationship"
);
var view = new View.EntryView()
{
DataContext = viewModel
};
view.Show();
}
}
}
您现在可以运行您的项目,尽管我们构建的逻辑不会做任何事情。这是因为我们创建了初始视图,但它实际上并没有进行任何数据绑定。
设置数据绑定
让我们返回并编辑视图以完成所有连接。
编辑视图/EntryView.xaml:
<Window x:Class="MyCompany.MyProject.View.EntryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Rolodex Entry"
Height="350" Width="525"
MinWidth="300" MinHeight="200">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Name:" Grid.Column="0" Grid.Row="0" />
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsInEditMode}" Grid.Column="1"
Grid.Row="0" Margin="6,0,0,0" />
<TextBlock Text="E-mail:" Grid.Column="0" Grid.Row="1"
Margin="0,6,0,0" />
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsInEditMode}" Grid.Column="1"
Grid.Row="1" Margin="6,6,0,0" />
<TextBlock Text="Phone Number:" Grid.Column="0" Grid.Row="2"
Margin="0,6,0,0" />
<TextBox Text="{Binding PhoneNumber, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsInEditMode}" Grid.Column="1" Grid.Row="2"
Margin="6,6,0,0" />
<TextBlock Text="Relationship:" Grid.Column="0" Grid.Row="3"
Margin="0,6,0,0" />
<TextBox Text="{Binding Relationship, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding IsInEditMode}" Grid.Column="1" Grid.Row="3"
Margin="6,6,0,0" />
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Content="{Binding CurrentEditModeName}"
Command="{Binding MakeEditableOrRevertCommand}"
Grid.Column="0" HorizontalAlignment="Left"
Width="75" />
<Button Content="Save" Command="{Binding SaveCommand}"
Grid.Column="1" Width="75" />
<Button Content="Cancel" Command="{Binding CancelCommand}"
Grid.Column="2" Width="75" Margin="6,0,0,0" />
</Grid>
</Grid>
</Window>
我在这里做了很多工作。首先,静态的东西:
- 我更改了表单的标题以匹配 Rolodex 的想法
- 我为字段添加了标签,因为我现在知道它们适用于什么
- 我更改了最小宽度/高度,因为我注意到控件被切断了
接下来是数据绑定:
- 我将所有文本字段绑定到视图模型上的适当属性
- 我创建了文本字段update the view model on every keypress (
UpdateSourceTrigger=PropertyChanged)。这对于此应用程序不是必需的,但将来可能会有所帮助。我添加它是为了避免您在需要时查找它:)
- 我将每个文本框的
IsEnabled 字段绑定到IsInEditMode 属性
- 我将按钮绑定到它们各自的命令
- 我将编辑按钮的名称(
Content 属性)绑定到视图模型上的相应属性
这是结果
现在所有 UI 逻辑都可以正常工作了,除了我们留下了 Todo 评论的那些。我没有实现这些,因为它们与特定的应用程序架构有关,我不想在这个演示中涉及到这些。
此外,香草 WPF 没有一种非常干净的 MVVM 方式来关闭我所知道的表单。您可以使用代码隐藏来执行此操作,也可以使用数十个 WPF 附加库之一,这些库提供了自己的更简洁的执行方式。
依赖属性
您可能已经注意到我没有在我的代码中创建单个自定义依赖属性。我使用的依赖属性都在现有控件上(例如Text、Content 和Command)。这就是它通常在 WPF 中的工作方式,因为数据绑定和样式(我没有涉及)为您提供了很多选择。它让您可以完全自定义内置控件的外观、感觉和操作。
在以前的 Windows GUI 框架中,您通常必须继承现有控件或创建自定义控件以获得自定义外观。在 WPF 中制作自定义控件的唯一原因是以可重用的方式组合多个控件的模式,或者从头开始创建一个全新的控件。
例如如果您正在制作一个与弹出控件配对的自动完成文本框以显示它自动完成的值。在这种情况下,您可能希望使用自定义依赖属性(例如自动完成源)制作自定义控件。这样您就可以在整个应用程序和其他应用程序中重复使用该控件。
如果您不制作自定义控件,或制作可以直接实例化并在 XAML 和数据绑定中使用的特殊非 UI 类,您可能不需要创建依赖属性。