问题
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:
- Flash when data changes
- Support "scrubbing" (hold and move mouse to increment/decrement value)
- 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:
- Dependency objects are generally the target of bindings
- Data should be independent of visualization
- 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