Handling a UserControl's DependencyProperty Command

前端 未结 1 914
迷失自我
迷失自我 2021-01-15 18:25

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

Here\'s a an ex

相关标签:
1条回答
  • 2021-01-15 18:47

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

    0 讨论(0)
提交回复
热议问题