getter/setter是普通C#属性的一种特性,它并不是WPF独有的。
单向/双向绑定是指WPF数据绑定,它不需要你创建依赖属性 - 只需要使用它们即可。
依赖属性已经内置于控件本身中。当您向表单添加自定义控件实例时,它们允许您直接引用这些属性。它们使您的自定义控件感觉更加“本地化”。
通常它们用于实现可以使用数据绑定的属性。在您的应用程序中,您将主要使用数据绑定,而不是为其实现新的钩子。
…如果有人点击添加或编辑按钮,则所有数据输入字段都将变为“启用”状态,其中包括新记录的空白数据或编辑现有数据。同时,添加/编辑按钮将变为禁用状态,但保存/取消按钮现在将变为启用状态。
同样地,完成后通过保存/取消,它将禁用所有输入字段,保存/取消,并重新启用添加/编辑按钮。
我会使用以下方式来实现您想要实现的功能:
- 一个视图模型
- 将数据绑定到该视图模型上的视图
- 在该视图模型上公开ICommand(用于按钮)
- 在视图模型上使用INotifyPropertyChanged(适用于所有属性)
在此场景中,不需要创建新的依赖属性。您只需使用现有的依赖属性进行数据绑定即可。
以下是使用数据绑定和MVVM样式进行WPF的代码示例/教程。
设置项目
我在“新建项目”向导中创建了一个WPF应用程序,并将其命名为MyProject
。
我设置了我的项目名称和命名空间以匹配通常接受的方案。您应该在解决方案资源管理器中设置这些属性 -> 项目 -> 右键单击 -> 属性。
![Project settings to set the correct namespaces](https://istack.dev59.com/e8VeD.webp)
我也有一种自定义的文件夹方案,我喜欢在 WPF 项目中使用:
![enter image description here](https://istack.dev59.com/hB5xx.webp)
出于组织目的,我将视图放在自己的“View”文件夹中。这也反映在命名空间中,因为您的命名空间应该与您的文件夹匹配 (namespace MyCompany.MyProject.View
)。
我还编辑了AssemblyInfo.cs,并清理了我的程序集引用和应用配置,但这只是一些乏味的工作,我会留给读者自己练习 :)
创建视图
首先进入设计器,让一切看起来很好。暂时不要添加任何代码,也不要做其他工作。只需在设计器中玩耍,直到一切看起来正确(尤其是当您调整大小时)。这是我最终得到的东西:
![The view I ended up with](https://istack.dev59.com/2yedc.webp)
View/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>
View/EntryView.xaml.cs:
using System.Windows;
namespace MyCompany.MyProject.View
{
public partial class EntryView : Window
{
public EntryView()
{
InitializeComponent();
}
}
}
我没有在这些控件上创建任何
Name
属性,这是有意的。我将使用MVVM,并且不会使用任何代码后台。我会让设计师做它想做的事情,但我不会触碰任何那些代码。
创建视图模型
接下来,我将制作我的视图模型。这应该以一种服务于视图的方式进行设计,但理想情况下可以是视图独立的。我不会太担心这个问题,但重点是你不需要在视图控件和视图模型对象之间有1对1的对应关系。
我试图使我的视图/视图模型在更大的应用程序上下文中有意义,所以我将从这里开始为视图模型定目标。我们将把这个"可编辑表单"作为一个转盘条目。
我们将首先创建一个需要的辅助类...
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)
{
Name = _initialName;
Email = _initialEmail;
PhoneNumber = _initialPhoneNumber;
Relationship = _initialRelationship;
}
IsInEditMode = !IsInEditMode;
}
private bool CanEditOrRevert()
{
return true;
}
private void Save()
{
AssertEditMode(isInEditMode: true);
IsInEditMode = false;
}
private bool CanSave()
{
return IsInEditMode;
}
private void Cancel()
{
AssertEditMode(isInEditMode: true);
IsInEditMode = false;
}
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)
{
var viewModel = new ViewModel.EntryViewModel(
"some name", "some email", "some phone number",
"some relationship"
);
var view = new View.EntryView()
{
DataContext = viewModel
};
view.Show();
}
}
}
你现在可以运行你的项目,但我们构建的逻辑不会做任何事情。这是因为我们创建了初始视图,但它实际上没有进行任何数据绑定。
设置数据绑定
让我们返回并编辑视图以完成所有连接。
编辑View/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 的概念
- 我添加了字段的标签,因为我现在知道它们适用于什么
- 我更改了最小宽度/高度,因为我注意到控件被裁剪了
接下来是数据绑定:
- 我将所有文本字段绑定到视图模型上相应的属性
- 我使文本字段 在每次按键时更新视图模型 (
UpdateSourceTrigger=PropertyChanged
)。虽然对于此应用程序来说这不是必需的,但在未来可能会有帮助。 我添加了它,以免您需要查找它 :)
- 我将每个文本框的
IsEnabled
字段绑定到 IsInEditMode
属性
- 我将按钮绑定到它们各自的命令
- 我将编辑按钮的名称 (
Content
属性) 绑定到视图模型上相应的属性
这是结果
![Edit mode](https://istack.dev59.com/PKxdI.webp)
现在所有的UI逻辑都可以工作,除了那些我们留下了“Todo”注释的部分。我没有实现它们,因为它们涉及特定的应用程序架构,而我不想在这个演示中深入探讨。
此外,普通的WPF没有一个非常干净的MVVM方式来关闭一个窗体。你可以使用代码后台来完成它,或者你可以使用数十个WPF附加库中的一个,提供自己更清晰的完成方式。
依赖属性
您可能已经注意到,在我的代码中,我没有创建一个自定义的依赖属性。我使用的依赖属性都是存在于现有控件上的(例如Text、Content和Command)。这通常是WPF中的工作方式,因为数据绑定和样式(我没有深入研究)给您提供了很多选项。它使您完全可以自定义内置控件的外观、感觉和操作。
在以前的Windows GUI框架中,您经常需要对现有控件进行子类化或创建自定义控件才能获得自定义的外观和感觉。在WPF中制作自定义控件的唯一原因是以可重复使用的方式结合多个控件的模式,或从头开始创建一个全新的控件。
例如,如果您正在制作一个自动完成文本框,并将其与弹出控件配对以显示它正在自动完成的值。在这种情况下,您可能需要创建一个自定义控件,具有自定义依赖属性(例如自动完成源)。这样,您可以在整个应用程序和其他应用程序中重复使用该控件。
如果您不是在制作自定义控件或制作特殊的非UI类,可以直接在XAML中实例化和使用数据绑定,则可能不需要创建依赖属性。