Handling a UserControl's DependencyProperty Command

旧巷老猫 提交于 2019-12-01 10:57:09

问题


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.


回答1:


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. :)



来源:https://stackoverflow.com/questions/58052597/handling-a-usercontrols-dependencyproperty-command

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!