Обработка команды DependencyProperty UserControl - PullRequest
1 голос
/ 22 сентября 2019

Я пытаюсь заставить WPF UserControl обновлять одно из его DependencyProperty при вызове DependencyProperty Команда .

Вот пример, который, я надеюсь, может продемонстрировать то, чего я пытаюсь достичь,По сути это пользовательский элемент управления с кнопкой на нем.Когда кнопка нажата, я хотел бы увеличить целое число (MyValue), используя команду (MyCommand):

Пользовательский элемент управления

<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 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, но я не знаю, где / как добавить код для обработки вызова Command.

То, что я пробовал до сих пор

Я могу просто обработать событие 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 класс не показан - это стандартная реализация команды делегата)

Этот последний подход работает, но поскольку команда является общей, она влияет на другие экземпляры этого пользовательского элемента управления.Например, если у меня есть 5 экземпляров, нажатие на 3-й экземпляр приведет к увеличению значения MyValue по предыдущему (2-му) экземпляру в XAML (но не в других экземплярах).

Любые указатели будут высоко оценены.


РЕДАКТИРОВАТЬ 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 , оба значения увеличиваются, но результат отображается в пользовательском элементе управления над нажатием.

Если Iудалить Shared в моих объявлениях, значения больше не обновляются:

Public Property IncrementValueCommand As ICommand
Public Property IncrementLocalValueCommand As ICommand

Вот где я застрял с этим подходом.Если бы это можно было мне объяснить, я был бы очень признателен.

Что касается создания View Model для обработки логики пользовательского элемента управления, это было бы здорово, я держался в стороне от этого, потому чтоСудя по тому, что я прочитал, это «вонь кода», поэтому я старался держаться подальше от этого подхода.

Чтобы немного прояснить свою цель: я пытаюсь создать пользовательский элемент управления Label, который может отображать дваЭлементы управления вверх / вниз, один для небольших приращений и один для больших приращений.Метка будет иметь много других функций, таких как:

  1. Вспышка при изменении данных
  2. Поддержка «очистки» (удерживайте и перемещайте мышь для увеличения / уменьшения значения)
  3. Естьвыделенное свойство, которое изменяет цвет фона метки.

Кажется, что подход View Model имеет смысл содержать всю эту логику.

1 Ответ

1 голос
/ 23 сентября 2019

Ваша последняя попытка очень близка к работоспособному решению.Это сработало бы, если бы вы просто не сделали свойство Shared.В самом деле, вы могли бы даже просто присвоить экземпляр RelayCommand существующему свойству зависимости MyCommand вместо создания нового свойства.

Тем не менее, неясно, что вы получите от такого подхода.Пользовательский элемент управления не станет универсальным, и вы могли бы реализовать этот подход с гораздо более простым в реализации обработчиком событий для события Button элемента Click.Итак, вот некоторые другие мысли относительно вашего вопроса и кода, содержащегося в…

Во-первых, для объекта зависимости WPF очень необычно реализовать INotifyPropertyChanged, и еще более необычно для него сделать это дляего свойства зависимости.Если вы решите сделать это, вместо того, чтобы делать это, как здесь, путем вызова события из самого установщика свойств, вы должны вместо этого включить обратный вызов изменения свойства при регистрации свойства зависимости, например, так:

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 обычно обновляет значение свойства зависимости напрямую, без прохождения через установщик свойства.В опубликованном вами коде это означает, что событие PropertyChanged не будет вызываться, поскольку свойство обновляется через привязку.Вместо этого вам нужно сделать это, как указано выше, чтобы убедиться, что любое изменение свойства приведет к возникновению события PropertyChanged.

При этом я бы не советовалреализация INotifyPropertyChanged для объектов зависимости.Сценарии, в которых можно создать объект зависимости, как правило, являются взаимоисключающими с необходимостью реализации INotifyPropertyChanged, поскольку объекты зависимости обычно являются целью привязки, тогда как INotifyPropertyChanged используется для объектов, которые являются источником привязки.Единственный компонент, которому необходимо наблюдать за изменением значения свойства в целевом объекте привязки, - это система привязки WPF, и он может делать это без реализации объектом зависимости 1025 *.

Во-вторых, более идиоматично.Чтобы реализовать то, что вы намеревались сделать здесь, было бы иметь отдельный объект модели представления, в котором будут храниться фактическое значение и команду, и привязать свойства модели представления к свойствам объекта зависимости.В этом случае можно получить объект модели представления, который будет выглядеть примерно так:

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

(Примечание: этот класс включает метод _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="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.Например:

<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.Сами данные теперь хранятся отдельно.В этом примере я только что использовал простой ресурс массива, но в более реалистичной программе это часто будет коллекция, на которую ссылается модель представления верхнего уровня, используемая в качестве контекста данных для окна.В любом случае, коллекция используется как значение свойства ItemsSource в ItemsControl.

(Примечание: для статических коллекций, как здесь, достаточно массива. Но класс ObservableCollection<T> очень часто используется вWPF, чтобы обеспечить источник привязки для коллекций, которые могут быть изменены во время выполнения программы.)

Затем объект ItemsControl использует шаблон данных, предоставленный для свойства ItemTemplate, для визуального представления представления.объект модели.

В этом примере шаблон данных уникален для этого объекта ItemsControl.Может быть желательно предоставить другой шаблон данных в другом месте, либо в другом ItemsControl, либо при индивидуальном представлении объектов модели представления (например, через ContentControl).Этот подход хорошо работает для таких сценариев.

Но также возможно, что можно иметь стандартную визуализацию для объекта модели представления.В этом случае можно определить используемый по умолчанию шаблон, поместив его в словарь ресурсов где-нибудь, чтобы WPF мог автоматически найти его в любом контексте, где можно использовать объект модели представления в качестве контекста данных.Тогда нет необходимости явно указывать шаблон в элементах пользовательского интерфейса, где это имеет место.

Это будет выглядеть примерно так:

<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такие как свойства зависимостей, привязка данных, шаблоны и т. д. На мой взгляд, следует иметь в виду следующие ключевые моменты:

  1. Объекты зависимостей обычно являются целью привязок
  2. Данные должны быть независимымивизуализации
  3. Не повторяйте себя.

Этот последний является критическим моментом во всем программировании, он лежит в основе ООП, и дажеболее простые сценарии, в которых вы можете создавать многократно используемые структуры данных и функции.Но в таких средах, как WPF, существует целый ряд новых аспектов, в которых есть возможность многократного использования вашего кода.Если вы обнаружите, что копируете / вставляете что-либо, имеющее отношение к вашей программе, вы, вероятно, нарушаете этот очень важный принцип.:)

...