处理 UserControl 的 DependencyProperty 命令
Handling a UserControl's DependencyProperty Command
我正在努力让 WPF UserControl 在调用 DependencyProperty Command 时更新其 DependencyProperty 之一。
这里有一个示例,有望展示我正在努力实现的目标。基本上它是一个带有按钮的用户控件。单击按钮时,我想使用命令 (MyCommand
):
递增一个整数 (MyValue
)
用户控制
<UserControl x:Class="UserControl1"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="100"
d:DesignWidth="200">
<Button x:Name="MyButton"
Content="{Binding MyValue, ElementName=root}"
Command="{Binding MyCommand, ElementName=root}" />
</UserControl>
代码隐藏 目前看起来是这样的:
Imports System.ComponentModel
Public Class UserControl1
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("MyValue", GetType(Integer), GetType(UserControl1), New PropertyMetadata(1))
Public Shared ReadOnly CommandProperty As DependencyProperty = DependencyProperty.Register("MyCommand", GetType(ICommand), GetType(UserControl1))
Public Property Value() As Integer
Get
Return GetValue(ValueProperty)
End Get
Set(value As Integer)
SetValue(ValueProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Value"))
End Set
End Property
Public Property Command() As ICommand
Get
Return CType(GetValue(CommandProperty), ICommand)
End Get
Set(value As ICommand)
SetValue(CommandProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Command"))
End Set
End Property
End Class
最后,我将此控件的 5 个实例添加到 Window:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Grid>
<StackPanel>
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
</StackPanel>
</Grid>
</Window>
我想要做的是让每个控件在单击按钮时将其 MyValue
递增 1。我已将 Button 的命令绑定到 MyCommand
以执行此操作,但我不知道 where/how 添加代码来处理命令调用。
到目前为止我尝试了什么
我可以简单地处理按钮上的 Click 事件:
Private Sub HandleButtonClick() Handles MyButton.Click
Value += 1
End Sub
这很好用,但我想通过 MyCommand 绑定来处理这个问题,以努力将代码隐藏限制在最低限度。
我尝试过的另一种方法是创建命令(不是作为 DependencyProperty):
Public Shared Property DirectCommand As ICommand
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
DirectCommand = New RelayCommand(Sub() Value += 1)
End Sub
(RelayCommand
class 未显示 - 这是委托命令的标准实现)
最后一种方法有效,但由于命令是共享的,它会影响此用户控件的其他实例。例如,如果我有 5 个实例,单击第 3 个实例将增加 XAML(但不是其他实例)中前一个(第 2 个)实例的 MyValue。
任何指点将不胜感激。
编辑 1:进一步使用非 DP 命令
按照@peter-duniho 的建议,我继续使用 RelayCommands 来处理按钮点击,但我没有运气让按钮调用未标记为共享的命令:
Public Class UserControl1
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("MyValue", GetType(Integer), GetType(UserControl1), New PropertyMetadata(1))
Private _localValue As Integer = 2
Public Shared Property IncrementValueCommand As ICommand
Public Shared Property IncrementLocalValueCommand As ICommand
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
IncrementValueCommand = New RelayCommand(Sub() Value += 1)
IncrementLocalValueCommand = New RelayCommand(Sub() LocalValue += 1)
End Sub
Public Property Value() As Integer
Get
Return GetValue(ValueProperty)
End Get
Set(value As Integer)
SetValue(ValueProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Value"))
End Set
End Property
Public Property LocalValue() As Integer
Get
Return _localValue
End Get
Set(value As Integer)
If _localValue <> value Then
_localValue = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("LocalValue"))
End If
End Set
End Property
End Class
我已经添加了一个 LocalValue 来尝试在没有 DependencyProperties 的情况下执行此操作,所以我现在有两个按钮可以并排测试:
<UserControl x:Class="UserControl1"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="100"
d:DesignWidth="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Button Grid.Row="0"
Background="DodgerBlue"
Content="{Binding Value, ElementName=root}"
Command="{Binding IncrementValueCommand, ElementName=root}" />
<Button Grid.Row="1"
Background="Gold"
Content="{Binding LocalValue, ElementName=root}"
Command="{Binding IncrementLocalValueCommand, ElementName=root}" />
</Grid>
</UserControl>
使用 Shared 命令,两个值都会递增,但结果显示在用户控件中单击的值上方。
如果我在声明中删除 Shared,值将不再更新:
Public Property IncrementValueCommand As ICommand
Public Property IncrementLocalValueCommand As ICommand
这就是我坚持使用这种方法的地方。如果可以向我解释一下,我将不胜感激。
至于创建一个 视图模型 来处理用户控件的逻辑,那太好了,我远离它,因为根据我的阅读,它是 "code stink" 所以我试图远离这种方法。
详细说明我的目标:我正在尝试制作一个可以显示两个 Up/Down 控件的标签用户控件,一个用于小增量,一个用于大增量。 Label 将具有许多其他功能,例如:
- 数据改变时闪烁
- 支持"scrubbing"(按住并移动鼠标到increment/decrement值)
- 有一个突出显示 属性 更改标签的背景颜色。
视图模型方法似乎非常适合包含所有这些逻辑。
您的最后一次尝试非常接近可行的解决方案。如果您只是没有将 属性 设为 Shared
属性,它就会起作用。事实上,您甚至可以将 RelayCommand
实例分配给现有的 MyCommand
依赖项 属性 而不是创建新的 属性.
也就是说,不清楚您将从这种方法中获得什么。用户控件最终不会成为通用控件,您可以使用 Button
元素的 Click
事件的更易于实现的事件处理程序来实现该方法。因此,这里有一些关于您的问题和其中包含的代码的其他想法……
首先,WPF 依赖对象实现 INotifyPropertyChanged
非常不寻常,更不寻常的是它的依赖属性。如果一个人决定这样做,而不是像你在这里那样做,通过从 属性 setter 本身引发事件,你 必须 而不是包含一个 属性-注册依赖时更改回调属性,像这样:
Public Shared ReadOnly CommandProperty As DependencyProperty =
DependencyProperty.Register("MyCommand", GetType(ICommand), GetType(UserControl1), New PropertyMetadata(AddressOf OnCommandPropertyChanged))
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub _RaisePropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Private Shared Sub OnCommandPropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim userControl As UserControl1 = CType(d, UserControl1)
userControl._RaisePropertyChanged(e.Property.Name)
End Sub
WPF 绑定系统通常直接更新依赖项 属性 值,而不通过 属性 setter。在您发布的代码中,这意味着 PropertyChanged
事件不会随着 属性 通过绑定更新而引发。相反,您需要像上面那样做,以确保 any 更改为 属性 将导致引发 PropertyChanged
事件。
也就是说,我建议不要为依赖对象实施 INotifyPropertyChanged
。制作依赖对象的场景通常与需要实现 INotifyPropertyChanged
互斥,因为依赖对象通常是绑定的目标,而 INotifyPropertyChanged
用于作为绑定源的对象捆绑。唯一需要观察绑定目标中 属性 值变化的组件是 WPF 绑定系统,它可以在没有实现 INotifyPropertyChanged
.
的依赖对象的情况下做到这一点
其次,一种更惯用的方式来实现您打算在此处执行的操作是拥有一个单独的视图模型对象,其中将存储实际值和命令,并将该视图模型的属性绑定到依赖项对象的属性。在那种情况下,会有一个看起来像这样的视图模型对象:
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Public Class UserControlViewModel
Implements INotifyPropertyChanged
Private _value As Integer
Public Property Value() As Integer
Get
Return _value
End Get
Set(value As Integer)
_UpdatePropertyField(_value, value)
End Set
End Property
Private _command As ICommand
Public Property Command() As ICommand
Get
Return _command
End Get
Set(value As ICommand)
_UpdatePropertyField(_command, value)
End Set
End Property
Public Sub New()
Command = New RelayCommand(Sub() Value += 1)
End Sub
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub _UpdatePropertyField(Of T)(ByRef field As T, newValue As T, <CallerMemberName> Optional propertyName As String = Nothing)
If Not EqualityComparer(Of T).Default.Equals(field, newValue) Then
field = newValue
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End If
End Sub
End Class
(注意:此 class 包含一个 _UpdatePropertyField()
方法,它处理实际的 属性 更改机制。通常,实际上会将此方法放入基 class ,因此您可以在可能编写的任何视图模型对象中重用该逻辑。)
在上面的示例中,视图模型将自己的 Command
属性 设置为 RelayCommand
对象。如果这是唯一想要支持的预期场景,那么可以将 属性 设置为只读。通过上面的实现,还可以选择将默认 ICommand
值替换为其他一些 ICommand
选择的对象(不同的 RelayCommand
或 ICommand
的任何其他实现).
定义了这个视图模型对象后,就可以为每个用户控件提供自己的视图模型作为数据上下文,将视图模型的属性绑定到用户控件的依赖属性:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
</StackPanel>
</Window>
每个用户控件对象都有自己的视图模型对象,将 XAML 初始化为 DataContext
属性 值。然后 {Binding Value}
和 {Binding Command}
标记导致视图模型属性充当每个用户控件对象的依赖性 属性 目标的来源。
这对于 WPF 来说更符合习惯。然而,实际上这仍然不是人们通常会如何去做的,因为所有的视图模型都是为用户控制对象硬编码的。当一个人拥有一组源对象,并希望以可视化方式表示它们时,通常会通过使用模板和 ItemsControl
UI 元素来保持数据和 UI 之间的分离。例如:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<x:Array x:Key="data" Type="{x:Type l:UserControlViewModel}">
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
</x:Array>
</Window.Resources>
<ItemsControl ItemsSource="{StaticResource data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type l:UserControlViewModel}">
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
在这里,StackPanel
以前作为元素明确安装在 window 中,现在用作 ItemsControl
元素中面板的模板。数据本身现在单独存储。在这个例子中,我只使用了一个简单的数组资源,但在一个更现实的程序中,这通常是一个由顶级视图模型引用的集合,用作 window 的数据上下文。无论哪种方式,集合都被用作 ItemsControl
.
中的 ItemsSource
属性 值
(注意:对于此处的静态集合,一个数组就足够了。但是 ObservableCollection<T>
class 在 WPF 中非常常用,为可能在执行期间修改的集合提供绑定源程序的执行。)
然后 ItemsControl
对象使用为 ItemTemplate
属性 提供的数据模板来直观地呈现视图模型对象。
在此示例中,数据模板对于该 ItemsControl
对象是唯一的。可能需要在其他地方提供不同的数据模板,或者在不同的 ItemsControl
中,或者在单独呈现视图模型对象时(例如通过 ContentControl
)。这种方法适用于这些场景。
但是,视图模型对象也可能具有标准的可视化效果。在这种情况下,可以定义要使用的默认模板,将其放在某个资源字典中,这样 WPF 就可以在任何可能使用视图模型对象作为数据上下文的上下文中自动找到它。然后,在这种情况下,不需要在 UI 元素中明确指定模板。
看起来像这样:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<x:Array x:Key="data" Type="{x:Type l:UserControlViewModel}">
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
</x:Array>
<DataTemplate DataType="{x:Type l:UserControlViewModel}">
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}"/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{StaticResource data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Window>
这只是 WPF 主题的皮毛,如依赖属性、数据绑定、模板等。我认为要记住的一些关键点是:
- 依赖对象通常是绑定的目标
- 数据应该独立于可视化
- 不要重复自己。
最后一个是所有编程的关键点,是 OOP 的核心,甚至是您可以创建可重用数据结构和函数的更简单的场景。但是在像 WPF 这样的框架中,有一个全新的维度范围,在这些维度中有机会重用您的代码。如果您发现自己 copy/pasting 任何与您的程序相关的事情,您可能违反了这个非常重要的原则。 :)
我正在努力让 WPF UserControl 在调用 DependencyProperty Command 时更新其 DependencyProperty 之一。
这里有一个示例,有望展示我正在努力实现的目标。基本上它是一个带有按钮的用户控件。单击按钮时,我想使用命令 (MyCommand
):
MyValue
)
用户控制
<UserControl x:Class="UserControl1"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="100"
d:DesignWidth="200">
<Button x:Name="MyButton"
Content="{Binding MyValue, ElementName=root}"
Command="{Binding MyCommand, ElementName=root}" />
</UserControl>
代码隐藏 目前看起来是这样的:
Imports System.ComponentModel
Public Class UserControl1
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("MyValue", GetType(Integer), GetType(UserControl1), New PropertyMetadata(1))
Public Shared ReadOnly CommandProperty As DependencyProperty = DependencyProperty.Register("MyCommand", GetType(ICommand), GetType(UserControl1))
Public Property Value() As Integer
Get
Return GetValue(ValueProperty)
End Get
Set(value As Integer)
SetValue(ValueProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Value"))
End Set
End Property
Public Property Command() As ICommand
Get
Return CType(GetValue(CommandProperty), ICommand)
End Get
Set(value As ICommand)
SetValue(CommandProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Command"))
End Set
End Property
End Class
最后,我将此控件的 5 个实例添加到 Window:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Grid>
<StackPanel>
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
<local:UserControl1 Width="40"
Height="40" />
</StackPanel>
</Grid>
</Window>
我想要做的是让每个控件在单击按钮时将其 MyValue
递增 1。我已将 Button 的命令绑定到 MyCommand
以执行此操作,但我不知道 where/how 添加代码来处理命令调用。
到目前为止我尝试了什么
我可以简单地处理按钮上的 Click 事件:
Private Sub HandleButtonClick() Handles MyButton.Click
Value += 1
End Sub
这很好用,但我想通过 MyCommand 绑定来处理这个问题,以努力将代码隐藏限制在最低限度。
我尝试过的另一种方法是创建命令(不是作为 DependencyProperty):
Public Shared Property DirectCommand As ICommand
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
DirectCommand = New RelayCommand(Sub() Value += 1)
End Sub
(RelayCommand
class 未显示 - 这是委托命令的标准实现)
最后一种方法有效,但由于命令是共享的,它会影响此用户控件的其他实例。例如,如果我有 5 个实例,单击第 3 个实例将增加 XAML(但不是其他实例)中前一个(第 2 个)实例的 MyValue。
任何指点将不胜感激。
编辑 1:进一步使用非 DP 命令
按照@peter-duniho 的建议,我继续使用 RelayCommands 来处理按钮点击,但我没有运气让按钮调用未标记为共享的命令:
Public Class UserControl1
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("MyValue", GetType(Integer), GetType(UserControl1), New PropertyMetadata(1))
Private _localValue As Integer = 2
Public Shared Property IncrementValueCommand As ICommand
Public Shared Property IncrementLocalValueCommand As ICommand
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
IncrementValueCommand = New RelayCommand(Sub() Value += 1)
IncrementLocalValueCommand = New RelayCommand(Sub() LocalValue += 1)
End Sub
Public Property Value() As Integer
Get
Return GetValue(ValueProperty)
End Get
Set(value As Integer)
SetValue(ValueProperty, value)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Value"))
End Set
End Property
Public Property LocalValue() As Integer
Get
Return _localValue
End Get
Set(value As Integer)
If _localValue <> value Then
_localValue = value
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("LocalValue"))
End If
End Set
End Property
End Class
我已经添加了一个 LocalValue 来尝试在没有 DependencyProperties 的情况下执行此操作,所以我现在有两个按钮可以并排测试:
<UserControl x:Class="UserControl1"
x:Name="root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="100"
d:DesignWidth="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Button Grid.Row="0"
Background="DodgerBlue"
Content="{Binding Value, ElementName=root}"
Command="{Binding IncrementValueCommand, ElementName=root}" />
<Button Grid.Row="1"
Background="Gold"
Content="{Binding LocalValue, ElementName=root}"
Command="{Binding IncrementLocalValueCommand, ElementName=root}" />
</Grid>
</UserControl>
使用 Shared 命令,两个值都会递增,但结果显示在用户控件中单击的值上方。
如果我在声明中删除 Shared,值将不再更新:
Public Property IncrementValueCommand As ICommand
Public Property IncrementLocalValueCommand As ICommand
这就是我坚持使用这种方法的地方。如果可以向我解释一下,我将不胜感激。
至于创建一个 视图模型 来处理用户控件的逻辑,那太好了,我远离它,因为根据我的阅读,它是 "code stink" 所以我试图远离这种方法。
详细说明我的目标:我正在尝试制作一个可以显示两个 Up/Down 控件的标签用户控件,一个用于小增量,一个用于大增量。 Label 将具有许多其他功能,例如:
- 数据改变时闪烁
- 支持"scrubbing"(按住并移动鼠标到increment/decrement值)
- 有一个突出显示 属性 更改标签的背景颜色。
视图模型方法似乎非常适合包含所有这些逻辑。
您的最后一次尝试非常接近可行的解决方案。如果您只是没有将 属性 设为 Shared
属性,它就会起作用。事实上,您甚至可以将 RelayCommand
实例分配给现有的 MyCommand
依赖项 属性 而不是创建新的 属性.
也就是说,不清楚您将从这种方法中获得什么。用户控件最终不会成为通用控件,您可以使用 Button
元素的 Click
事件的更易于实现的事件处理程序来实现该方法。因此,这里有一些关于您的问题和其中包含的代码的其他想法……
首先,WPF 依赖对象实现 INotifyPropertyChanged
非常不寻常,更不寻常的是它的依赖属性。如果一个人决定这样做,而不是像你在这里那样做,通过从 属性 setter 本身引发事件,你 必须 而不是包含一个 属性-注册依赖时更改回调属性,像这样:
Public Shared ReadOnly CommandProperty As DependencyProperty =
DependencyProperty.Register("MyCommand", GetType(ICommand), GetType(UserControl1), New PropertyMetadata(AddressOf OnCommandPropertyChanged))
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub _RaisePropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Private Shared Sub OnCommandPropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim userControl As UserControl1 = CType(d, UserControl1)
userControl._RaisePropertyChanged(e.Property.Name)
End Sub
WPF 绑定系统通常直接更新依赖项 属性 值,而不通过 属性 setter。在您发布的代码中,这意味着 PropertyChanged
事件不会随着 属性 通过绑定更新而引发。相反,您需要像上面那样做,以确保 any 更改为 属性 将导致引发 PropertyChanged
事件。
也就是说,我建议不要为依赖对象实施 INotifyPropertyChanged
。制作依赖对象的场景通常与需要实现 INotifyPropertyChanged
互斥,因为依赖对象通常是绑定的目标,而 INotifyPropertyChanged
用于作为绑定源的对象捆绑。唯一需要观察绑定目标中 属性 值变化的组件是 WPF 绑定系统,它可以在没有实现 INotifyPropertyChanged
.
其次,一种更惯用的方式来实现您打算在此处执行的操作是拥有一个单独的视图模型对象,其中将存储实际值和命令,并将该视图模型的属性绑定到依赖项对象的属性。在那种情况下,会有一个看起来像这样的视图模型对象:
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Public Class UserControlViewModel
Implements INotifyPropertyChanged
Private _value As Integer
Public Property Value() As Integer
Get
Return _value
End Get
Set(value As Integer)
_UpdatePropertyField(_value, value)
End Set
End Property
Private _command As ICommand
Public Property Command() As ICommand
Get
Return _command
End Get
Set(value As ICommand)
_UpdatePropertyField(_command, value)
End Set
End Property
Public Sub New()
Command = New RelayCommand(Sub() Value += 1)
End Sub
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub _UpdatePropertyField(Of T)(ByRef field As T, newValue As T, <CallerMemberName> Optional propertyName As String = Nothing)
If Not EqualityComparer(Of T).Default.Equals(field, newValue) Then
field = newValue
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End If
End Sub
End Class
(注意:此 class 包含一个 _UpdatePropertyField()
方法,它处理实际的 属性 更改机制。通常,实际上会将此方法放入基 class ,因此您可以在可能编写的任何视图模型对象中重用该逻辑。)
在上面的示例中,视图模型将自己的 Command
属性 设置为 RelayCommand
对象。如果这是唯一想要支持的预期场景,那么可以将 属性 设置为只读。通过上面的实现,还可以选择将默认 ICommand
值替换为其他一些 ICommand
选择的对象(不同的 RelayCommand
或 ICommand
的任何其他实现).
定义了这个视图模型对象后,就可以为每个用户控件提供自己的视图模型作为数据上下文,将视图模型的属性绑定到用户控件的依赖属性:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}">
<l:UserControl1.DataContext>
<l:UserControlViewModel Value="1"/>
</l:UserControl1.DataContext>
</l:UserControl1>
</StackPanel>
</Window>
每个用户控件对象都有自己的视图模型对象,将 XAML 初始化为 DataContext
属性 值。然后 {Binding Value}
和 {Binding Command}
标记导致视图模型属性充当每个用户控件对象的依赖性 属性 目标的来源。
这对于 WPF 来说更符合习惯。然而,实际上这仍然不是人们通常会如何去做的,因为所有的视图模型都是为用户控制对象硬编码的。当一个人拥有一组源对象,并希望以可视化方式表示它们时,通常会通过使用模板和 ItemsControl
UI 元素来保持数据和 UI 之间的分离。例如:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<x:Array x:Key="data" Type="{x:Type l:UserControlViewModel}">
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
</x:Array>
</Window.Resources>
<ItemsControl ItemsSource="{StaticResource data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type l:UserControlViewModel}">
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
在这里,StackPanel
以前作为元素明确安装在 window 中,现在用作 ItemsControl
元素中面板的模板。数据本身现在单独存储。在这个例子中,我只使用了一个简单的数组资源,但在一个更现实的程序中,这通常是一个由顶级视图模型引用的集合,用作 window 的数据上下文。无论哪种方式,集合都被用作 ItemsControl
.
ItemsSource
属性 值
(注意:对于此处的静态集合,一个数组就足够了。但是 ObservableCollection<T>
class 在 WPF 中非常常用,为可能在执行期间修改的集合提供绑定源程序的执行。)
然后 ItemsControl
对象使用为 ItemTemplate
属性 提供的数据模板来直观地呈现视图模型对象。
在此示例中,数据模板对于该 ItemsControl
对象是唯一的。可能需要在其他地方提供不同的数据模板,或者在不同的 ItemsControl
中,或者在单独呈现视图模型对象时(例如通过 ContentControl
)。这种方法适用于这些场景。
但是,视图模型对象也可能具有标准的可视化效果。在这种情况下,可以定义要使用的默认模板,将其放在某个资源字典中,这样 WPF 就可以在任何可能使用视图模型对象作为数据上下文的上下文中自动找到它。然后,在这种情况下,不需要在 UI 元素中明确指定模板。
看起来像这样:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO58052597CommandProperty"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<x:Array x:Key="data" Type="{x:Type l:UserControlViewModel}">
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
<l:UserControlViewModel Value="1"/>
</x:Array>
<DataTemplate DataType="{x:Type l:UserControlViewModel}">
<l:UserControl1 Width="40" Height="40" MyValue="{Binding Value}" MyCommand="{Binding Command}"/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{StaticResource data}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Window>
这只是 WPF 主题的皮毛,如依赖属性、数据绑定、模板等。我认为要记住的一些关键点是:
- 依赖对象通常是绑定的目标
- 数据应该独立于可视化
- 不要重复自己。
最后一个是所有编程的关键点,是 OOP 的核心,甚至是您可以创建可重用数据结构和函数的更简单的场景。但是在像 WPF 这样的框架中,有一个全新的维度范围,在这些维度中有机会重用您的代码。如果您发现自己 copy/pasting 任何与您的程序相关的事情,您可能违反了这个非常重要的原则。 :)