Как правильно привязать свойство IsPressed к параметру моей команды?

Я сделал пользовательскую кнопку для привязки команды к (настраиваемому, маршрутизируемому) событию IsPressedChanged, чтобы команда выполнялась как при нажатии кнопки, так и при ее отпускании:

<local:CustomButton xmlns:i = "http://schemas.microsoft.com/xaml/behaviors" x:Name = "MyButton">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName = "CustomIsPressedChanged">
            <i:InvokeCommandAction Command = "{Binding Path=SomeCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</local:CustomButton>

С реализацией пользовательской кнопки:

public partial class CustomButton : Button
    {
        /* Register a custom routed event using the bubble routing strategy. */
        public static readonly RoutedEvent CustomIsPressedChangedEvent = EventManager.RegisterRoutedEvent(
            name: "CustomIsPressedChanged",
            routingStrategy: RoutingStrategy.Bubble,
            handlerType: typeof(RoutedEventHandler),
            ownerType: typeof(CustomButton));

        /* Provide CLR accessors for assigning an event handler. */
        public event RoutedEventHandler CustomIsPressedChanged
        {
            add { AddHandler(CustomIsPressedChangedEvent, value); }
            remove { RemoveHandler(CustomIsPressedChangedEvent, value); }
        }

        public CustomButton() { InitializeComponent(); }

        /* Custom Event handling of the IsPressedChanged event */
        protected override void OnIsPressedChanged(System.Windows.DependencyPropertyChangedEventArgs e)
        {
            /* Call the base class OnIsPressedChanged() method so IsPressedChanged event subscribers are notified. */
            base.OnIsPressedChanged(e);

            /* Raise custom event */
            RaiseEvent(new RoutedEventArgs(routedEvent: CustomIsPressedChangedEvent));
        }
    }

Это работает отлично, как и должно.

И теперь возникает Проблема:

Когда я пытаюсь передать значение свойства IsPressed команде следующим образом:

<i:InvokeCommandAction Command = "{Binding Path=SomeCommand}"
                       CommandParameter = "{Binding ElementName=MyButton, Path=IsPressed}"/>

распространяемое значение (по-видимому) всегда будет старым значением IsPressed. Когда я нажимаю кнопку, команда вызывается с параметром false, когда я отпускаю кнопку, параметр становится истинным. Но, когда я проверяю значение IsPressed внутри обработчика событий CustomButton.OnIsPressedChanged(), оно представляет новое значение, как и ожидалось.

Мой вопрос: Как я должен распространять значение IsPressed, чтобы получить правильное значение? Гарантируется ли, что команда всегда будет вызываться со старым значением? В этом случае я мог бы просто инвертировать значение, но мне это кажется немного сомнительным, и я бы действительно не хотел этого делать, если не знаю, что это всегда даст правильный результат.

Я бы полностью избегал Interaction.Triggers. Вместо этого я бы привязал IsPressed к свойству модели представления, используя это решение, и вызвал бы требуемый метод команды в установщике свойств.

ASh 13.05.2022 18:32

Нельзя ли привязать команду непосредственно к свойству Button.Command? Это было бы самым простым решением. IsPressed истинно только в течение очень короткого момента, когда кнопка активирована. Похоже, что триггер оценивается после того, как событие завершило свой обход. Кстати, если вам нужно только состояние Pressed==True, вы можете отфильтровать его и поднять событие только в этом случае. Это устраняет требование параметра.

BionicCode 13.05.2022 19:08

Если вам нужны оба состояния, то лучшим решением будет реализовать событие для каждого состояния: Pressed и Released. Это всегда лучше, чем передача состояния через аргументы события. Вы фильтруете состояние в методе OnIsPressedChanged, а затем вызываете соответствующее событие.

BionicCode 13.05.2022 19:08

Вы также можете использовать eventTrigger для событий мыши вверх и вниз вместо реализации пользовательской кнопки. Отличается ли команда для мыши вверх от команды для мыши вниз?

XAMlMAX 13.05.2022 23:10

@BionicCode, Button.Command может вызывать команду только при нажатии или отпускании, поэтому я не могу использовать ее здесь. - Что касается поведения значения IsPressed: это может зависеть от конфигурации кнопки, но для моей реализации IsPressed верно, пока я удерживаю кнопку мыши.

Felix 16.05.2022 09:30

@BionicCode, создание разных событий для нажатия и выпуска - хорошая идея. Он инкапсулирует функциональность и упрощает вещи за пределами реализации кнопки.

Felix 16.05.2022 09:32

@XAMlMAX, события подъема и опускания мыши, к сожалению, не работают, когда кнопка запускается клавиатурой, что во многих случаях нежелательно.

Felix 16.05.2022 09:34

@ Феликс Теперь я понимаю. Вы должны знать, что у кнопки есть свойство Button.ClickMode. Это свойство (внутренняя фильтрация) заставляет кнопку выполняться только один раз при нажатии, отпускании или наведении. Вот почему вы испытываете поведение с одним щелчком мыши. IsPressed сбрасывается после завершения цикла события. Это означает, что кнопка уже выполнила событие click. Позвольте мне опубликовать простое решение для вас.

BionicCode 16.05.2022 11:03

Ах, вы должны добавить эту информацию к вашему вопросу. И похоже, что вы используете команды для статистики? Я не уверен, что могу дать вам хороший совет, но окончание команды при запуске команды означает, что кнопка отпущена. Если это не кнопка переключения? Немного больше информации, почему вы пытаетесь это сделать, может дать нам лучшее понимание.

XAMlMAX 16.05.2022 22:25

Я также уверен, что вы можете использовать возможность выполнения и выполнения вашей команды в качестве точек входа и выхода. Просто пытаюсь облегчить вам задачу, поэтому требуется меньше работы. ХТН

XAMlMAX 16.05.2022 22:27

@XAMlMAX, я хочу выполнять какие-то действия, пока нажата кнопка, и сразу же останавливаться, как только кнопка отпущена. Здесь нет статистики. Кроме того, я не понимаю, что вы подразумеваете под «концом команды», как здесь может помочь CanExecute.

Felix 17.05.2022 08:54
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
11
76
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы можете передать DependencyPropertyChangedEventArgs как параметр поднятого RoutedEventArgs:

protected override void OnIsPressedChanged(DependencyPropertyChangedEventArgs e)
{
    base.OnIsPressedChanged(e);

    // you may want to pass e.NewValue here for simplicity.
    RaiseEvent(new RoutedEventArgs(CustomIsPressedChangedEvent, e));
}

Затем попросите InvokeCommandAction передать его команде:

<i:InvokeCommandAction Command = "{Binding Path=SomeCommand}"
                       PassEventArgsToCommand = "True" />

А затем в команде только что вам нужно привести переданный объект, чтобы получить новое значение IsPressed:

SomeCommand = new ActionCommand(SomeCommandAction);

//...

private void SomeCommandAction(object o)
{
    if (o is not RoutedEventArgs routedEventArgs)
        return;

    if (routedEventArgs.OriginalSource is not DependencyPropertyChangedEventArgs eventArgs)
        return;

    if (eventArgs.NewValue is true)
        Count++;

    if (eventArgs.NewValue is false)
        Count--;

}

Рабочая демонстрация здесь.

Отлично, это работает. К сожалению, это заставляет модель представления обрабатывать объект RoutedEventArgs, который немного запутан. Я бы хотел, чтобы модель представления обрабатывала только логическое значение.

Felix 16.05.2022 12:17

@Felix, вы можете использовать преобразователь (атрибут EventArgsConverter), чтобы извлечь новое значение из RoutedEventArgs. Но на данный момент присоединенное свойство чище.

Orace 16.05.2022 13:43

@Феликс. Я добавил прикрепленный пример свойства здесь

Orace 16.05.2022 14:04

спасибо за этот полный пример. Я не совсем понимаю каждую мелочь, но это нормально.

Felix 17.05.2022 08:30

Из соображений удобства (для использования вашего элемента управления) вы не должны реализовывать параллельную команду. Вместо этого измените существующее поведение.

Кнопка имеет свойство Button.ClickMode. Внутренняя фильтрация этого свойства кнопки заставляет Button выполняться только один раз на ClickMode.Press, ClickMode.Release или ClickMode.Hover.
Нам нужно обойти эту фильтрацию, чтобы выполнить событие Button.Command и Button.Click как для MouseLeftButtonDown, так и для MouseLeftButtonUp (для реализации ClickMode.Press и ClickMode.Release), а также для MouseEnter и MouseLeave (для поддержки ClickMode.Hover):

DoubleTriggerButton.cs

public class DoubleTriggerButton : Button
{
  public bool IsDoubleTriggerEnabled
  {
    get => (bool)GetValue(IsDoubleTriggerEnabledProperty);
    set => SetValue(IsDoubleTriggerEnabledProperty, value);
  }

  public static readonly DependencyProperty IsDoubleTriggerEnabledProperty = DependencyProperty.Register(
    "IsDoubleTriggerEnabled",
    typeof(bool),
    typeof(DoubleTriggerButton),
    new PropertyMetadata(true));

  protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode != ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeftButtonDown(e);
    }
  }

  protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode != ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeftButtonUp(e);
    }
  }

  protected override void OnMouseEnter(MouseEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode == ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseEnter(e);
    }
  }

  protected override void OnMouseLeave(MouseEventArgs e)
  {
    if (this.IsDoubleTriggerEnabled
      && this.ClickMode == ClickMode.Hover)
    {
      base.OnClick();
    }
    else
    {
      base.OnMouseLeave(e);
    }
  }
}

Я нашел другое решение, которое почти не нуждается в изменениях по сравнению с моим исходным кодом:

Редактировать: Как отметил BionicCode в комментариях, это не очень хороший дизайн по нескольким причинам.

Добавляя свойство зависимости к CustmButton, которое заменяет свойство IsPressed, можно присвоить правильное значение внутри обработчика событий OnIsPressedChanged. Затем привязка к новому свойству IsPressed работает так, как я ожидал, что исходное свойство будет работать:

public new static readonly DependencyProperty IsPressedProperty =
    DependencyProperty.Register("IsPressed", typeof(bool), typeof(CustomButton),
        new PropertyMetadata(false));

public new bool IsPressed
{
    get { return (bool)GetValue(IsPressedProperty); }
    set { SetValue(IsPressedProperty, value); }
}

protected override void OnIsPressedChanged(System.Windows.DependencyPropertyChangedEventArgs e)
{
    /* Call the base class OnIsPressedChanged() method so IsPressedChanged event subscribers are notified. */
    base.OnIsPressedChanged(e);

    /* Forward the value of the base.IsPressed property to the custom IsPressed property  */
    IsPressed = (bool)e.NewValue;

    /* Raise event */
    RaiseCustomRoutedEvent(new RoutedEventArgs(routedEvent: CustomIsPressedChangedEvent));
}

Теперь можно связать параметр команды с пересылаемым новым значением:

<local:CustomButton xmlns:i = "http://schemas.microsoft.com/xaml/behaviors" x:Name = "MyButton">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName = "CustomIsPressedChanged">
            <i:InvokeCommandAction Command = "{Binding Path=SomeCommand}"
                                   CommandParameter = "{Binding ElementName=MyButton, Path=IsPressed}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</local:CustomButton>

Это не очень хороший дизайн. И его ужасно использовать. Вы можете использовать эту кнопку только после прочтения ее документации, чтобы узнать, что она полностью обходит исходный API (например, свойство Command или событие Click). Вам всегда придется настраивать CustomButton перед каждым использованием с триггером взаимодействия, который работает только с очень конкретным событием. С точки зрения дизайна класса вы не расширили кнопку, но параллельно реализовали совершенно новое поведение обхода, которое делает бесполезным исходный API кнопки полный!.

BionicCode 17.05.2022 11:02

Вы должны оставить исходный API для удобства использования. Затем измените поведение, переопределив соответствующие виртуальные элементы. Таким образом, кнопку можно использовать как обычно. Как показывает мой пример, вы даже можете сделать новое поведение необязательным и по-прежнему разрешать использование события Click (которое также вызывается дважды).

BionicCode 17.05.2022 11:02

@BionicCode большое спасибо за вашу оценку. Я еще не думал о том, что происходит, когда CustomButton нужно повторно использовать где-то еще. Будет ли эта проблема частично решена, если я переименую новое свойство зависимостей IsPressed во что-то вроде CustomIsPressed, чтобы оно не мешало исходному свойству?

Felix 17.05.2022 11:09

@BionicCode также, как этот дизайн нарушает исходное свойство Command и событие Click (кроме того, что возится со свойством IsPressed)? Кажется, он все еще работает для меня.

Felix 17.05.2022 11:46

Не совсем. Дело в том, что если бы мне пришлось использовать вашу кнопку, я бы использовал ее неправильно. Функция приятная. Но не очень хорошо спроектирован. Если я получу CustomButton, который на самом деле является Button, я привяжу команду к свойству Command или назначу обработчик события Click событию. Просто чтобы понять, что CustomButton не ведет себя так, как рекламируется, и не будет выполнять команду или обработчик событий дважды. Даже если я сам автор, я не вспомню этот поворот через несколько месяцев.

BionicCode 17.05.2022 11:48

Теперь, поддерживая старый код, я не мог понять, почему я использовал сложный триггер взаимодействия вместо свойства Command. Это плохо разработанный API (обход исходного API и, кроме того, требование триггеров взаимодействия) и плохо разработанный класс (вместо переопределения поведения он реализует функции, требующие обхода исходного поведения и API). Переименование свойства не меняет класс и структуру API. Если вам удастся настроить триггер взаимодействия внутри (через C#), вы сможете, по крайней мере, устранить это неудобное требование к конфигурации.

BionicCode 17.05.2022 11:48

Он сломан, поскольку CustomButton является Button (наследством). Но исходный API, такой как Command или Click, не имеет функциональности в производном CustomButton. Пользователь CustomButton вынужден использовать другой API для выполнения команды. Но поскольку CustomButton — это Button, выполнение команды все равно должно происходить через свойство Command. Только поведение изменилось, например. способ или момент, когда это Command вызывается. В настоящее время приведение CustomButton к Button нарушит управление, поскольку свойство Button.Command не используется.

BionicCode 17.05.2022 11:52

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

BionicCode 17.05.2022 11:56

@BionicCode большое спасибо за ваш отзыв и подробное объяснение. Это очень ценится!

Felix 17.05.2022 12:51

Другие вопросы по теме