命令绑定与事件 - 从简单到高级的方式






4.38/5 (13投票s)
描述了命令、附加属性以及将控件事件与命令绑定的巧妙方法。
引言
命令是WPF中一个非常强大的工具。此外,当涉及到WPF中的MVVM模式时,命令成为执行ViewModel中定义的、由View中的控件触发的操作的绝佳工具。我假设本文的读者熟悉WPF中的MVVM、RelayCommand和ICommand,尽管我将尝试保持简单以帮助理解。
在MVVM中使用命令
在本节中,我将逐步展示如何
- 在ViewModel中声明一个命令
- 如何将窗口中的控件与该命令绑定
目标:我们的目标是将一个按钮控件与ViewModel中声明的命令绑定。该命令将调用ViewModel中定义的自定义函数。
在ViewModel中声明一个命令
在ViewModel中声明命令很简单。只需声明一个ICommand类型的属性,并使用RelayCommand来委托ViewModel中定义的操作。
public class StudentModel : ViewModelBase { // other codes in the model. //…. // the callback function of the Compare commad void CompareExecute(object prm) { if (this.Grade == "F") this.Name = "Rizvi - Not " + prm.ToString(); if (this.Grade == "A+") this.Name = "Rizvi - " + prm.ToString(); } private RelayCommand _Compare; public ICommand Compare { get { if (this._Compare == null) this._Compare = new RelayCommand(CompareExecute); return this._Compare; } } }
- 这里有两点需要注意:
new RelayCommand(CompareExecute);
- 在这里,RelayCommand类的构造函数接受一个函数指针。当按钮调用该命令时,该函数将被实际执行。void CompareExecute(object prm) { … }
– 这个函数接受一个object类型的参数。这个参数将由按钮控件的CommandParameter绑定提供。
嗯,此时我们可以在RelayCommand声明中使用lambda表达式。如果你的回调函数非常简单,比如改变一个属性或者只是比较两个值,那么你可以使用lambda表达式。只需将lambda表达式作为RelayCommand初始化构造函数的参数,如下所示。
public class StudentModel : ViewModelBase { // other codes in the model. //…. private RelayCommand _Compare; public ICommand Compare { get { if (this._Compare == null) this._Compare = new RelayCommand((prm) => { this.Grade = "A+"; }); return this._Compare; } } }
如何绑定窗口中的控件
现在是时候将按钮控件与该命令绑定了。我们简单地这样做,如下所示。
<button command="{Binding Compare}" commandparameter="Good Student" content="Evaluate" name="button1" />
- 这里又有两点需要注意:
Command="{Binding Compare}"
– 通过这个,我们实际上是将按钮的点击事件与命令Compare
绑定。CommandParameter="Good Student"
– 通过这个,你实际上提供了一个命令参数,它将被传递到模型中定义的函数 - voidCompareExecute(object prm) { … }
在步骤1中。
到目前为止,这是命令在MVVM中的一个非常简单的实现,通过这种方法,你可以将任何具有命令属性的控件与ViewModel中的函数绑定。
附加属性的兴起
命令在MVVM中非常棒。但命令仍然存在一些限制。首先,命令不能绑定到不实现ICommandSource接口的控件。换句话说,在没有实现ICommandSource的控件中,你找不到Command属性来分配绑定。例如,你会在XAML的Label元素中找不到像Button元素那样的命令属性。其次,你被迫将命令绑定到使用ICommandSource实现的控件事件。例如,你只能将Button控件的点击事件与命令绑定,而不能将LostFocus事件绑定。第三,你不能将一个事件与控件中的命令绑定。例如,你无法将MouseEnter事件与命令绑定。基本上,你无法编写像这样的控件元素
<button MouseEnterCommand="{Binding MyCommand}" MouseEnterCommandParameter="{Binding MyCommandParameter}" content="Button1" name="button1" />
为了克服这些限制,有一个非常简单的解决方法——使用依赖属性。起初,依赖属性可能会显得非常复杂(至少对我来说),但事实并非如此。至少在这篇文章中,我将尽量保持简单。
首先,让我们熟悉依赖属性。依赖属性是一种机制,你可以在控件中注册一个全新的属性。当你在一个静态类中注册这个属性,并将其用于或附加到另一个控件或类时,这个依赖属性就变成了一个附加属性。在这篇文章中,我不会详细阐述依赖属性。相反,我将推荐这些链接(自定义依赖对象和依赖属性和如何:注册附加属性)关于依赖属性和附加属性,但肯定我会简单地展示如何使用它。
目标:注册一个依赖属性。将该属性附加到Label控件。将此附加属性绑定到ViewModel属性。
准备工作
如果你按照我的项目CommandExamples2
,你会发现一些实现Model-View-ViewModel模式的基本准备工作。为了保持示例简单,我创建了一个Student
类,其中包含Id
、Name
和Advice
属性。这就是我们的模型。然后我创建了StudentModel
类,它实现了ViewModelBase
接口——因此,根据MVVM在WPF中的概念,这是可以理解的,它将成为一个ViewModel。在这个ViewModel中,有一个LocalStudent
属性,类型为Student
。为了简单起见,我们将使用MainWindow
作为我们的View,并在其中将datacontext
设置为ViewModel实例,也就是StudentModel
实例。你可以在窗口的Loaded事件中或在XAML中设置这个datacontext
。
在MainWindow
中,我有一个Label。在这个Label
元素中,我想引入一个全新的属性叫做Show,它将绑定到我们模型中的LocalStudent
属性,并以特殊的方式显示学生信息。这就是依赖属性的作用。我将在另一个共享类MyAttachedBehaviour
中创建并注册这个Show
属性,这个类通常被称为Provider Class。这个Provider Class实际上将向控件提供附加属性;在我们的例子中,就是Label
,如下所示。
<Label local:MyAttachedBehaviour.Show="{Binding LocalStudent}" Content="Label1" Name="Label1"></Label>
现在,在MyAttachedBehaviour中注册show属性包含三个步骤
- 属性注册,
- 属性的Getter和Setter 3. 属性的更改处理程序
- 属性的更改处理程序
属性注册
注册非常简单,就像调用DependencyProperty
类中的RegisterAttached
函数一样。
public static class MyAttachedBehaviour
{
public static DependencyProperty ShowProperty = DependencyProperty.RegisterAttached(
"Show",
typeof(Student),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata(new Student(0,"","X"), new PropertyChangedCallback(MyAttachedBehaviour.ShowChanged)));
… Other codes coming …
}
前三个参数不言自明,所以我认为第四个参数需要一些解释。这个FrameworkPropertyMetadata
在构造函数中有两个参数。第一个是属性的默认值——换句话说,这个值将在属性注册后立即加载。在我们的例子中,默认值为new Student(0,"","X")
。
第二个参数用于指向一个回调函数,该函数将在属性更改时执行。在我们的例子中,ShowChanged
将在属性值更改时执行。它是通过PropertyChangedCallback
对象提供的。
属性的Getter和Setter
public static class MyAttachedBehaviour
{
public static DependencyProperty ShowProperty = DependencyProperty.RegisterAttached(
"Show",
typeof(Student),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata(new Student(0,"","X"), new PropertyChangedCallback(MyAttachedBehaviour.ShowChanged)));
public static void SetShow(DependencyObject target, Student value)
{
target.SetValue(MyAttachedBehaviour.ShowProperty, value);
}
public static Student GetShow(DependencyObject target)
{
return (Student)target.GetValue(MyAttachedBehaviour.ShowProperty);
}
… Other codes coming …
}
Getter和Setter遵循一个约定:get[attachedPropertyName]和set[attachedPropertyName]。这些访问器是必需的,以便XAML读取器可以将该属性识别为XAML中的一个属性,并解析适当的类型。注意,它们应该调用Getvalue
和SetValue
来处理数据。target
参数在执行时实际上指向我们的Label。
属性的更改处理程序
更改处理程序相对简单。通过FrameworkPropertyMetadata
对象,我附加了一个属性更改处理程序ShowChanged
。在这个处理程序中,我实际上是从Label的Show
属性中窃取新值,并将其设置为该Label
中的content
属性为字符串。
public static class MyAttachedBehaviour
{
public static DependencyProperty ShowProperty = DependencyProperty.RegisterAttached(
"Show",
typeof(Student),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata(new Student(0,"","X"), new PropertyChangedCallback(MyAttachedBehaviour.ShowChanged)));
public static void SetShow(DependencyObject target, Student value)
{
target.SetValue(MyAttachedBehaviour.ShowProperty, value);
}
public static Student GetShow(DependencyObject target)
{
return (Student)target.GetValue(MyAttachedBehaviour.ShowProperty);
}
private static void ShowChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
Label element = target as Label;
if (element!=null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
Student _s = e.NewValue as Student;
element.Content = _s.Id.ToString() + " # " + _s.Name + " # " + _s.Grade;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.Content = string.Empty;
}
}
}
}
现在是时候运行我们的CommandExample2
项目并查看输出了。
MainWindow
中有一个“显示学生”按钮。在该按钮的Code-behind点击事件处理程序中,我将我们的附加属性Show
设置为一个Student
对象。
思考题:你可能会想,如果我在附加属性注册中设置了默认值,为什么它在表单加载时不会显示出来呢?
答案:如果我们稍加调试,我们就会发现Label的附加属性Show
确实得到了默认值(new Student(0,"","X")
),但是一旦这个属性从与ViewModel的LocalStudent
属性的第一次绑定中获得一个空值,值就会改变——ShowChanged
处理程序被触发——Label的内容就变成了string.Empty
。
命令绑定与事件
如果你一直跟到这里,你应该对如何将一个控件与命令绑定有所了解,如果一个控件没有合适的属性可以绑定,我们可以创建一个(附加属性)。现在我们离将一个事件与ViewModel中的自定义命令绑定非常接近了。
目标:在ViewModel中定义一个命令属性。在控件中注册一个附加属性并将其与命令绑定。在附加属性的更改处理程序中,将事件处理程序钩接到控件的事件。
准备工作
我们的下一个示例项目CommandExample3
与前一个项目非常相似。这里有相同的Student
类,具有Id
、Name
和Grade
属性。同样是实现了ViewModelBase
接口的StudentModel
类,但在这个ViewModel中有三个属性:类型为Student
的LocalStudent
,类型为string
的Advice
,以及类型为ICommand
的命令属性QuickCommant
。在本篇文章的第一部分(在MVVM中使用命令)中,我们已经学会了如何在ViewModel中创建命令属性。我们应该在这里运用这些知识。我们的StudentModel如下。
public class StudentModel : ViewModelBase
{
public StudentModel()
{
this.LocalStudent = new Student(1, "Rizvi", "B");
}
private Student _LocalStudent;
public Student LocalStudent
{
get { return this._LocalStudent; }
set { if(value != this._LocalStudent) { this._LocalStudent = value; OnPropertyChanged("LocalStudent");} }
}
private string _Advice;
public string Advice
{
get { return this._Advice; }
set { if (value != this._Advice) { this._Advice = value; OnPropertyChanged("Advice"); } }
}
private RelayCommand _QuickCommant;
public ICommand QuickCommant
{
get
{
if (this._QuickCommant == null)
this._QuickCommant = new RelayCommand((f) => { var f1 = (StudentModel)f; f1.Advice = (f1.LocalStudent.Grade == "B" ? "Try to improve Geography":"No Commant"); });
return this._QuickCommant;
}
}
}
MainWindow
像以前一样用作View。有四个文本框用于显示Id、Name、Grade和Advice。在StudentModel
的构造函数中,初始化了一个Student
实例new Student(1, "Rizvi", "B")
,并在表单加载时,这些学生信息显示在文本框中。窗口中有一个Label
(鼠标敏感区域),它是鼠标敏感的,如果鼠标悬停在该Label
上,Advice
文本框就会填充一个建议文本。这是因为Label
的MouseEnter
事件绑定了StudentModel
中的QuickCommant
命令。该命令评估学生的Grade
并为StudentModel
的Advice属性设置一个建议文本。
创建附加属性
为了使Label对鼠标敏感,我们将不得不使用该控件的MouseEnter
事件。但我不会在Code-behind中编写这个事件处理程序。相反,我将为Label
控件注册一个名为MouseOverCommand
的附加属性。然后,我将把它的MouseEnter
事件与StudentModel
的QuickCommant
命令绑定。从上一节(附加属性的兴起)我们了解到如何将属性附加到控件。所以我这次不再描述,但你可以看到代码。
public class MyAttachedBehaviour
{
public static DependencyProperty MouseOverCommandProperty = DependencyProperty.RegisterAttached(
"MouseOverCommand",
typeof(ICommand),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata(
new PropertyChangedCallback(MyAttachedBehaviour.MouseOverChanged)));
public static void SetMouseOverCommand(DependencyObject target, ICommand value)
{
target.SetValue(MyAttachedBehaviour.MouseOverCommandProperty, value);
}
public static ICommand GetMouseOverCommand(DependencyObject target)
{
return (ICommand)target.GetValue(MyAttachedBehaviour.MouseOverCommandProperty);
}
… Other codes coming …
}
请注意,与之前描述的附加属性唯一区别是,这次附加属性的类型是ICommand
。这绝对是合理的,因为我们将它绑定到命令。
<label content="Mouse sencitive area" behave:myattachedbehaviour.mouseovercommand="{Binding QuickCommant}" behave:myattachedbehaviour.commandperam="{Binding}" background="#FFE8DF2E" />
现在我将向你展示如何在附加属性的更改处理程序中挂接事件处理程序。
挂接事件处理程序
观察下面的MyAttachedBehaviour
类。在我们的附加属性中,请注意属性更改处理程序是MouseOverChanged
。在这个处理程序中,target参数是Label
。所以,我们将其转换为UIElement
,并将处理程序element_MouseOver
挂接到它的MouseEnter
事件。然后在这个element_MouseOver
处理程序的第一行,我们从sender那里获取了UIElement
(我们的Label)。最重要的是,在第二行,我们获取了绑定到我们Label
的命令(我们的QuickCommant
命令)。最后,在第三行,我们实际上使用execute
方法执行了该命令。
public class MyAttachedBehaviour
{
public static DependencyProperty MouseOverCommandProperty = DependencyProperty.RegisterAttached(
"MouseOverCommand",
typeof(ICommand),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata( new PropertyChangedCallback(MyAttachedBehaviour.MouseOverChanged)));
public static void SetMouseOverCommand(DependencyObject target, ICommand value)
{
target.SetValue(MyAttachedBehaviour.MouseOverCommandProperty, value);
}
public static ICommand GetMouseOverCommand(DependencyObject target)
{
return (ICommand)target.GetValue(MyAttachedBehaviour.MouseOverCommandProperty);
}
private static void MouseOverChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
UIElement element = target as UIElement;
if (element!=null)
{
// If we're putting in a new command and there wasn't one already
// hook the event
if ((e.NewValue != null) && (e.OldValue == null))
{
element.MouseEnter += element_MouseOver;
}
// If we're clearing the command and it wasn't already null
// unhook the event
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.MouseEnter -= element_MouseOver;
}
}
}
static void element_MouseOver(object sender, MouseEventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(MyAttachedBehaviour.MouseOverCommandProperty);
command.Execute(element.GetValue(MyAttachedBehaviour.CommandPeramProperty));
}
… Other codes coming …
}
嗯,在这个execute方法中,我们传递了命令参数。为了获取这个命令参数,我们必须为Label注册另一个附加属性,名为CommandPeram
。
创建另一个附加属性
现在,我们必须为Label
创建另一个名为CommandPeram
的附加属性来绑定命令参数。在XAML中,我们将像这样使用这个属性。这里,通过{Binding}
,我们实际上将整个ViewModel(我们的StudentModel)绑定到了附加属性CommandPeram
。
<label content="Mouse sencitive area" behave:myattachedbehaviour.mouseovercommand="{Binding QuickCommant}" behave:myattachedbehaviour.commandperam="{Binding}" background="#FFE8DF2E"/>
在同一个Provider类MyAttachedBehaviour
中,注册一个名为CommandPeram
的新附加属性到Label相当直接。请观察“Command parameter section”注释下的文本区域。
public class MyAttachedBehaviour
{
public static DependencyProperty MouseOverCommandProperty = DependencyProperty.RegisterAttached(
"MouseOverCommand",
typeof(ICommand),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata( new PropertyChangedCallback(MyAttachedBehaviour.MouseOverChanged)));
public static void SetMouseOverCommand(DependencyObject target, ICommand value)
{
target.SetValue(MyAttachedBehaviour.MouseOverCommandProperty, value);
}
public static ICommand GetMouseOverCommand(DependencyObject target)
{
return (ICommand)target.GetValue(MyAttachedBehaviour.MouseOverCommandProperty);
}
private static void MouseOverChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
UIElement element = target as UIElement;
if (element!=null)
{
// If we're putting in a new command and there wasn't one already
// hook the event
if ((e.NewValue != null) && (e.OldValue == null))
{
element.MouseEnter += element_MouseOver;
}
// If we're clearing the command and it wasn't already null
// unhook the event
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.MouseEnter -= element_MouseOver;
}
}
}
static void element_MouseOver(object sender, MouseEventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(MyAttachedBehaviour.MouseOverCommandProperty);
command.Execute(element.GetValue(MyAttachedBehaviour.CommandPeramProperty));
}
// Command peremeter section
public static DependencyProperty CommandPeramProperty = DependencyProperty.RegisterAttached(
"CommandPeram",
typeof(object),
typeof(MyAttachedBehaviour),
new FrameworkPropertyMetadata(new PropertyChangedCallback(MyAttachedBehaviour.CommandPeramChanged)));
public static void SetCommandPeram(DependencyObject target, object value)
{
target.SetValue(MyAttachedBehaviour.CommandPeramProperty, value);
}
public static object GetCommandPeram(DependencyObject target)
{
return (object)target.GetValue(MyAttachedBehaviour.CommandPeramProperty);
}
private static void CommandPeramChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
UIElement element = target as UIElement;
if (element != null)
{
// If we're putting in a new command and there wasn't one already
// hook the event
if ((e.NewValue != null) && (e.OldValue == null))
{
element.SetValue(MyAttachedBehaviour.CommandPeramProperty, e.NewValue);
}
// If we're clearing the command and it wasn't already null
// unhook the event
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.SetValue(MyAttachedBehaviour.CommandPeramProperty, e.NewValue);
}
}
}
}
}
通过这个附加属性,我们实际上将我们的ViewModel(StudentModel
)作为命令属性提供给我们的命令QuickCommant
。在这个命令中,有一个用lambda表达式编写的命令处理程序。在那里,ViewModel被提取出来,一个值被设置给Advice属性。观察StudentModel
中的QuickCommant
命令,如下所示。
private RelayCommand _QuickCommant;
public ICommand QuickCommant
{
get
{
if (this._QuickCommant == null)
this._QuickCommant = new RelayCommand((f) => { var f1 = (StudentModel)f; f1.Advice = (f1.LocalStudent.Grade == "B" ? "Try to improve Geography":"No Commant"); });
return this._QuickCommant;
}
}
结束语
最后,我们可以总结一下我们到目前为止所做的一切。我们看到了如何使用一个简单的命令;然后我们学会了注册一个简单的附加属性。之后,我们使用这个附加属性将控件的事件与命令绑定。最后,我们学会了如何使用另一个附加属性将命令参数发送到这个命令。
这是我在WPF中将事件与命令绑定的方法。也可能还有其他方法。我欢迎你对本文的所有评论、建议或意见。