处理UserControl的DependencyProperty命令 [英] Handling a UserControl's DependencyProperty Command

查看:74
本文介绍了处理UserControl的DependencyProperty命令的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在努力让WPF用户控件在调用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>

<按钮x:Name = MyButton
Content = {Binding MyValue,ElementName = root}
Command = {Binding MyCommand,ElementName = root} />

< / UserControl>

到目前为止,隐藏代码看起来像这样:

 导入System.ComponentModel 
公共类UserControl1
实现INotifyPropertyChanged

公共事件PropertyChanged作为PropertyChangedEventHandler实现INotifyPropertyChanged.PropertyChanged

公共共享的只读ValueProperty,因为DependencyProperty = DependencyProperty.Register( MyValue,GetType(Integer),GetType(UserControl1),New PropertyMetadata(1))
公共共享ReadOnly CommandProperty as DependencyProperty = DependencyProperty.Register( MyCommand,GetType(ICommand),GetType(UserControl1))

公共属性Value()作为整数
获取
返回GetValue(ValueProperty )
结束获取
Set(值作为整数)
SetValue(ValueProperty,值)
RaiseEvent PropertyChanged(Me,New PropertyChangedEventArgs( Value))
结束集
结束属性

公共属性Command()作为ICommand
获取
返回CType(GetValue(CommandProperty),ICommand)
结束Get
Set(值作为ICommand)
SetValue(CommandProperty,值)
RaiseEvent PropertyChanged(Me,New PropertyChangedEventArgs( Command))
结束集
结束属性

终端类

最后,我在 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
山雀le = 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 来执行此操作,但是我不知道在哪里/如何添加代码来处理命令调用。



到目前为止我尝试过的事情



我可以简单地处理按钮上的Click事件:

  Private Sub HandleButtonClick()处理MyButton.Click 
值+ = 1
End Sub

这可以正常工作,但我想通过MyCommand绑定来处理此问题,以将代码隐藏量降至最低。



我尝试过的另一种方法是创建一个命令(而不是DependencyProperty):

  ICommand的公共共享属性DirectCommand 

Public Sub New()

'设计人员需要此调用。
InitializeComponent()

’在InitializeComponent()调用之后添加任何初始化。
DirectCommand =新的RelayCommand(Sub()值+ = 1)

End Sub

RelayCommand 类未显示-这是委托命令的标准实现)



此最后一种方法可行,但是由于命令是共享的,因此会影响此用户控件的其他实例。例如,如果我有5个实例,则单击第3个实例将使XAML中的第一个(第二个)实例上的MyValue递增(但不包含其他实例)。



任何指针将不胜感激。






编辑1:使用非DP命令更进一步



按照@ peter-duniho的建议,我继续使用RelayCommands来处理按钮单击,但是我没有运气让按钮来调用不是的命令。 t标记为共享:

 公共类UserControl1 
实现INotifyPropertyChanged

公共事件PropertyChanged为PropertyChangedEventHandler实现INotifyPropertyChanged.PropertyChanged

公共共享的只读ValueProperty,因为DependencyProperty = DependencyProperty.Register( MyValue,GetType(Integer),GetType(UserControl1),New PropertyMetadata(1))
私有_localValue作为整数= 2

Publ ic作为ICommand
的共享属性IncrementValueCommand作为ICommand

的公共共享属性IncrementLocalValueCommand作为公共子类New()

’设计人员需要此调用。
InitializeComponent()

’在InitializeComponent()调用之后添加任何初始化。
IncrementValueCommand =新的RelayCommand(Sub()值+ = 1)
IncrementLocalValueCommand =新的RelayCommand(Sub()局部值+ = 1)

结束子

公共属性Value()作为整数
获取
返回GetValue(ValueProperty)
End获取
Set(作为整数的值)
SetValue(ValueProperty,value)
RaiseEvent PropertyChanged(Me,New PropertyChangedEventArgs( Value))
结束集
结束属性

公共属性LocalValue()作为整数
获取
返回_localValue
结束Get
Set(值作为整数)
如果_localValue<> value然后
_localValue = value
RaiseEvent PropertyChanged(Me,New PropertyChangedEventArgs( LocalValue))
如果
End Set
End Property

终端类

我添加了LocalValue来尝试不使用DependencyProperties进行操作,所以现在有了两个按钮来同时测试两个按钮:

 < UserControl x:Class = UserControl1 
x:名称= 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>

<按钮Grid.Row = 0
Background = DodgerBlue
Content = {Binding Value,ElementName = root}
Command = {Binding IncrementValueCommand,ElementName = root} />

<按钮Grid.Row = 1
Background = Gold
Content = {Binding LocalValue,ElementName = root}
Command = {Binding IncrementLocalValueCommand,ElementName = root} />

< / Grid>

< / UserControl>

使用 Shared 命令,两个值都增加,但结果显示在用户控件上方的那一个单击。



如果我在声明中删除了Shared,则值不再更新:

 公共属性IncrementValueCommand作为ICommand 
公共属性IncrementLocalValueCommand作为ICommand

这就是我坚持这种方法的地方。如果可以向我解释一下,将不胜感激。



就创建 View Model 来处理用户控件的逻辑而言,太好了,我不要这样做了,因为从我所读的内容来看,这是代码恶臭,所以我试图不使用这种方法。



我的目标有一点:我正在尝试制作一个Label用户控件,该控件可以显示两个Up / Down控件,一个用于小增量,一个用于大增量。标签将具有许多其他功能,例如:


  1. 数据更改时闪烁

  2. 支持清理 (按住并移动鼠标以增加/减少值)。

  3. 具有突出显示属性,该属性会更改标签的背景颜色。

包含所有这些逻辑的视图模型方法似乎很有意义。

解决方案

最后一次尝试是非常接近可行的解决方案。如果您只是不将该属性设置为 Shared 属性,那就可以了。确实,您甚至可以将 RelayCommand 实例分配给现有的 MyCommand 依赖项属性,而不是创建新的属性。



也就是说,尚不清楚您将从这种方法中获得什么。用户控件不会成为通用的,您可以使用 Button 元素的<$ c更加易于实现的事件处理程序来实现该方法。 $ c>点击事件。因此,关于您的问题和…

中包含的代码,还有其他一些想法。首先,对于WPF依赖对象实现 INotifyPropertyChanged ,甚至更不寻常的是它的依赖属性。应该决定这样做,而不是像在这里那样做,通过从属性设置器本身引发事件,您必须在注册依赖项属性时包括一个属性更改回调,例如:

 作为DependencyProperty的公共共享只读CommandProperty = 
DependencyProperty.Register( MyCommand ,GetType(ICommand),GetType(UserControl1),新PropertyMetadata(AddressOf OnCommandPropertyChanged))

公共事件PropertyChanged作为PropertyChangedEventHandler实现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)

userCon trol._RaisePropertyChanged(e.Property.Name)
结束子

通常WPF绑定系统直接更新依赖项属性值,而无需通过属性设置器。在发布的代码中,这意味着通过绑定更新属性不会引发 PropertyChanged 事件。相反,您需要按照上述步骤进行操作,以确保对该属性进行任何更改都会导致引发 PropertyChanged 事件。



也就是说,我建议不要为依赖对象实现 INotifyPropertyChanged 。通常情况下,将要创建依赖对象的场景与需要实现 INotifyPropertyChanged 互斥,因为依赖对象通常是绑定的目标,而 INotifyPropertyChanged 用于作为绑定源的对象。 WPF绑定系统是唯一需要观察绑定目标中属性值变化的组件,并且无需依赖对象实现 INotifyPropertyChanged 就可以做到这一点。



第二种,您打算在此处实现的更惯用的方法是拥有一个单独的视图模型对象,用于存储实际值和命令,然后将该视图模型的属性绑定到依赖对象的属性。在那种情况下,将有一个看起来像这样的视图模型对象:

 导入系统.ComponentModel 
导入System.Runtime.CompilerServices

公共类UserControlViewModel
实现INotifyPropertyChanged

私有_value作为整数

Public属性Value()作为整数
获取
返回_value
End Get
Set(作为整数的值)
_UpdatePropertyField(_value,value)
End Set
结束属性

私有_Command作为ICommand

公共属性Command()作为ICommand
获取
返回_command
End获取
Set(值作为ICommand)
_UpdatePropertyField(_command,值)
结束集
结束属性

Public Sub New()
Command =新的RelayCommand(Sub()值+ = 1)
E nd Sub

公共事件PropertyChanged作为PropertyChangedEventHandler实现INotifyPropertyChanged.PropertyChanged

Private Sub _UpdatePropertyField(Of T)(ByRef字段为T,newValue为T,< CallerMemberName>可选propertyName As String = Nothing)
如果不是EqualityComparer(T).Default.Equals(field,newValue)然后
field = newValue
RaiseEvent PropertyChanged(Me,New PropertyChangedEventArgs(propertyName))
结束如果
结束子
结束类

(注意:此类包括一个处理实际属性更改机制的 _UpdatePropertyField()方法,通常,该方法实际上会将该方法放入基类中,因此您可以在任何视图模型对象可能会写。)



在上面的示例中,视图模型将自己的 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 = MainWind ow 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 明确地用作窗口中的元素,现在用作 ItemsControl 元素中面板的模板。现在,数据本身已单独存储。在此示例中,我仅使用了一个简单的数组资源,但在更实际的程序中,该资源通常是由顶层视图模型引用的集合,该模型用作窗口的数据上下文。无论哪种方式,该集合都将用作 ItemsControl 中的 ItemsSource 属性值。



(注意:对于此处的静态集合,数组就足够了。但是 ObservableCollection< T> 类在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/表达式/混合/ 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中诸如依赖项属性,数据绑定,模板等主题的表面。我认为要记住的一些关键点是:


  1. 依赖对象通常是绑定的目标

  2. 数据应该独立于可视化

  3. 不要重复自己。

最后一个是所有编程的关键点,并且是OOP的核心,甚至是更简单的场景,在这些场景中,您都可以创建可重用的数据结构和函数。但是在WPF之类的框架中,存在一个全新的维度范围,在其中可以重用您的代码。如果您发现自己复制/粘贴了与程序相关的任何内容,则可能违反了这一非常重要的原则。 :)


I am struggling with getting a WPF UserControl to update one of its DependencyProperty when a DependencyProperty Command is invoked.

Here's a an example that can hopefully demonstrate what I am trying to achieve. Basically it's a user control with a button on it. When the button is clicked, I'd like to increment an integer (MyValue) using a command (MyCommand):

User Control

<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>

The Code-behind looks like this so far:

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

Finally, I've added 5 instances of this control to a 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>

What I would like to do is have each control increment its MyValue by 1 when the button is clicked. I've bound the Button's command to MyCommand to do so but I do not know where/how to add code to handle the Command invocation.

What I have tried so far

I can simply handle the Click event on the button:

Private Sub HandleButtonClick() Handles MyButton.Click
    Value += 1
End Sub

This works fine but I would like to handle this through the MyCommand binding in an effort to limit code-behind to a minimum.

Another approach I have tried is to create a Command (not as 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 not shown - it's a standard implementation of a delegate command)

This last approach works but since the command is Shared it affects other instances of this user control. For example, if I have 5 instances, clicking 3rd instance will will increment the MyValue on the previous (2nd) instance in the XAML (but not other instances).

Any pointers would be greatly appreciated.


EDIT 1: Going further with non-DP Commands

Following @peter-duniho's advice, I continued down the path of using RelayCommands to handle the button click but I am having no luck getting the button to invoke a command that isn't marked as Shared:

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

I've added a LocalValue to try doing this with no DependencyProperties so I now have two buttons to test both side-by-side:

<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>

Using Shared commands, both values increment but the result is shown in the user control above the one clicked.

If I remove Shared in my declarations, the values don't update anymore:

Public Property IncrementValueCommand As ICommand
Public Property IncrementLocalValueCommand As ICommand

This is where I am stuck with this approach. If this can be explained to me I would be very grateful.

As far as creating a View Model to handle the User Control's logic, that would be great, I stayed away from that because, from what I have read, it's "code stink" so I was trying to stay away from that approach.

To elaborate a little on my goal: I am trying to make a Label user control that can display two Up/Down controls, one for small increments and one for larger increments. The Label will have many other features like:

  1. Flash when data changes
  2. Support "scrubbing" (hold and move mouse to increment/decrement value)
  3. Have a Highlighted property that changes the label's background color.

The View Model approach seems to make perfect sense to contain all this logic.

解决方案

Your last attempt is very close to a workable solution. It would have worked, had you simply not made the property a Shared property. Indeed, you could have even just assigned the RelayCommand instance to the existing MyCommand dependency property instead of creating a new property.

That said, it's not clear what you would gain from such an approach. The user control wouldn't wind up being general-purpose, and you could implement that approach with a much-simpler-to-implement event handler for the Button element's Click event. So, here are some other thoughts with respect to your question and the code contained within…

First, it is very unusual for a WPF dependency object to implement INotifyPropertyChanged, and even more unusual for it do so for its dependency properties. Should one decide to do so, instead of doing as you have here, by raising the event from the property setter itself, you must instead include a property-change callback when you register the dependency property, like so:

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

The WPF binding system typically updates a dependency property value directly, without going through the property setter. In the code you posted, this means that the PropertyChanged event would not be raised with the property is updated via a binding. Instead, you need to do it as above, to make sure that any change to the property will result in the PropertyChanged event being raised.

That said, I'd advise not implementing INotifyPropertyChanged for dependency objects. The scenarios where one would make a dependency object are generally mutually exclusive with needing to implement INotifyPropertyChanged, because dependency objects are typically the target of a binding, while INotifyPropertyChanged is used for objects which are the source of a binding. The only component that needs to observe the change of a property value in the target of a binding is the WPF binding system, and it can do that without the dependency object implementing INotifyPropertyChanged.

Second, a more idiomatic way to implement something as you've intended to do here would be to have a separate view model object where the actual value and command would be stored, and bind that view model's properties to the dependency object's properties. In that case, one would have a view model object that looks something like this:

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

(Note: this class includes an _UpdatePropertyField() method which handles the actual property change mechanism. Typically, one would actually put this method into a base class, so you can reuse that logic in any view model object one might write.)

In the example above, the view model sets its own Command property to the RelayCommand object. If this is the only intended scenario one wants to support, then one could just make the property read-only. With the implementation above, one also has the option of replacing the default ICommand value with some other ICommand object of choice (either a different RelayCommand or any other implementation of ICommand).

With this view model object defined, one can then give each user control its own view model as a data context, binding the view model's properties to the user control's dependency properties:

<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>

Each user control object gets its own view model object, initialized the XAML as the DataContext property value. Then the {Binding Value} and {Binding Command} markup cause the view model properties to serve as the source for the dependency property targets for each user control object.

This is a little more idiomatic for WPF. However, it's actually still not really how one would typically go about doing this, because all the view models are hard-coded for the user control objects. When one has a collection of source objects, and wants to represent them visually, one would typically maintain a separation between data and UI through the use of templating and the ItemsControl UI element. For example:

<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>

Here, the StackPanel which was previously installed explicitly as an element in the window, is now used as the template for the panel in an ItemsControl element. The data itself is now stored separately. In this example, I've just used a simple array resource, but in a more realistic program this would often be a collection referenced by a top-level view model used as the data context for the window. Either way, the collection gets used as the ItemsSource property value in the ItemsControl.

(Note: for static collections as here, an array suffices. But the ObservableCollection<T> class is very commonly used in WPF, to provide a binding source for collections that may be modified during the execution of the program.)

The ItemsControl object then uses the data template provided in for the ItemTemplate property to visually present the view model object.

In this example, the data template is unique for that ItemsControl object. It might be desirable to provide a different data template elsewhere, either in a different ItemsControl, or when presenting the view model objects individually (e.g. via ContentControl). This approach works well for those kinds of scenarios.

But, it is also possible that one might have a standard visualization for the view model object. In that case, one can define a default template to use, placing that in a resource dictionary somewhere, so that WPF can automatically find it in any context where one might be using the view model object as the data context. Then, no template needs to be specified explicitly in the UI elements where that's the case.

That would look something like this:

<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>

This just barely scratches the surface on topics in WPF like dependency properties, data binding, templating, etc. Some key points in my view to keep in mind are:

  1. Dependency objects are generally the target of bindings
  2. Data should be independent of visualization
  3. Don't repeat yourself.

That last one is a crucial point in all programming, and is at the heart of OOP, and even simpler scenarios where you can make reusable data structures and functions. But in frameworks like WPF, there is a whole new range of dimensions in which there's the opportunity for reusing your code. If you find yourself copy/pasting anything related to your program, you're probably violating this very important principle. :)

这篇关于处理UserControl的DependencyProperty命令的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆