Я сделал пользовательскую кнопку для привязки команды к (настраиваемому, маршрутизируемому) событию 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
, чтобы получить правильное значение? Гарантируется ли, что команда всегда будет вызываться со старым значением? В этом случае я мог бы просто инвертировать значение, но мне это кажется немного сомнительным, и я бы действительно не хотел этого делать, если не знаю, что это всегда даст правильный результат.
Нельзя ли привязать команду непосредственно к свойству Button.Command? Это было бы самым простым решением. IsPressed истинно только в течение очень короткого момента, когда кнопка активирована. Похоже, что триггер оценивается после того, как событие завершило свой обход. Кстати, если вам нужно только состояние Pressed==True, вы можете отфильтровать его и поднять событие только в этом случае. Это устраняет требование параметра.
Если вам нужны оба состояния, то лучшим решением будет реализовать событие для каждого состояния: Pressed и Released. Это всегда лучше, чем передача состояния через аргументы события. Вы фильтруете состояние в методе OnIsPressedChanged, а затем вызываете соответствующее событие.
Вы также можете использовать eventTrigger для событий мыши вверх и вниз вместо реализации пользовательской кнопки. Отличается ли команда для мыши вверх от команды для мыши вниз?
@BionicCode, Button.Command
может вызывать команду только при нажатии или отпускании, поэтому я не могу использовать ее здесь. - Что касается поведения значения IsPressed
: это может зависеть от конфигурации кнопки, но для моей реализации IsPressed
верно, пока я удерживаю кнопку мыши.
@BionicCode, создание разных событий для нажатия и выпуска - хорошая идея. Он инкапсулирует функциональность и упрощает вещи за пределами реализации кнопки.
@XAMlMAX, события подъема и опускания мыши, к сожалению, не работают, когда кнопка запускается клавиатурой, что во многих случаях нежелательно.
@ Феликс Теперь я понимаю. Вы должны знать, что у кнопки есть свойство Button.ClickMode
. Это свойство (внутренняя фильтрация) заставляет кнопку выполняться только один раз при нажатии, отпускании или наведении. Вот почему вы испытываете поведение с одним щелчком мыши. IsPressed сбрасывается после завершения цикла события. Это означает, что кнопка уже выполнила событие click. Позвольте мне опубликовать простое решение для вас.
Ах, вы должны добавить эту информацию к вашему вопросу. И похоже, что вы используете команды для статистики? Я не уверен, что могу дать вам хороший совет, но окончание команды при запуске команды означает, что кнопка отпущена. Если это не кнопка переключения? Немного больше информации, почему вы пытаетесь это сделать, может дать нам лучшее понимание.
Я также уверен, что вы можете использовать возможность выполнения и выполнения вашей команды в качестве точек входа и выхода. Просто пытаюсь облегчить вам задачу, поэтому требуется меньше работы. ХТН
@XAMlMAX, я хочу выполнять какие-то действия, пока нажата кнопка, и сразу же останавливаться, как только кнопка отпущена. Здесь нет статистики. Кроме того, я не понимаю, что вы подразумеваете под «концом команды», как здесь может помочь CanExecute
.
Вы можете передать 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, вы можете использовать преобразователь (атрибут EventArgsConverter
), чтобы извлечь новое значение из RoutedEventArgs
. Но на данный момент присоединенное свойство чище.
@Феликс. Я добавил прикрепленный пример свойства здесь
спасибо за этот полный пример. Я не совсем понимаю каждую мелочь, но это нормально.
Из соображений удобства (для использования вашего элемента управления) вы не должны реализовывать параллельную команду. Вместо этого измените существующее поведение.
Кнопка имеет свойство 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 кнопки полный!.
Вы должны оставить исходный API для удобства использования. Затем измените поведение, переопределив соответствующие виртуальные элементы. Таким образом, кнопку можно использовать как обычно. Как показывает мой пример, вы даже можете сделать новое поведение необязательным и по-прежнему разрешать использование события Click (которое также вызывается дважды).
@BionicCode большое спасибо за вашу оценку. Я еще не думал о том, что происходит, когда CustomButton нужно повторно использовать где-то еще. Будет ли эта проблема частично решена, если я переименую новое свойство зависимостей IsPressed
во что-то вроде CustomIsPressed
, чтобы оно не мешало исходному свойству?
@BionicCode также, как этот дизайн нарушает исходное свойство Command и событие Click (кроме того, что возится со свойством IsPressed)? Кажется, он все еще работает для меня.
Не совсем. Дело в том, что если бы мне пришлось использовать вашу кнопку, я бы использовал ее неправильно. Функция приятная. Но не очень хорошо спроектирован. Если я получу CustomButton
, который на самом деле является Button
, я привяжу команду к свойству Command
или назначу обработчик события Click
событию. Просто чтобы понять, что CustomButton
не ведет себя так, как рекламируется, и не будет выполнять команду или обработчик событий дважды. Даже если я сам автор, я не вспомню этот поворот через несколько месяцев.
Теперь, поддерживая старый код, я не мог понять, почему я использовал сложный триггер взаимодействия вместо свойства Command
. Это плохо разработанный API (обход исходного API и, кроме того, требование триггеров взаимодействия) и плохо разработанный класс (вместо переопределения поведения он реализует функции, требующие обхода исходного поведения и API). Переименование свойства не меняет класс и структуру API. Если вам удастся настроить триггер взаимодействия внутри (через C#), вы сможете, по крайней мере, устранить это неудобное требование к конфигурации.
Он сломан, поскольку CustomButton
является Button
(наследством). Но исходный API, такой как Command
или Click
, не имеет функциональности в производном CustomButton
. Пользователь CustomButton
вынужден использовать другой API для выполнения команды. Но поскольку CustomButton
— это Button
, выполнение команды все равно должно происходить через свойство Command
. Только поведение изменилось, например. способ или момент, когда это Command
вызывается. В настоящее время приведение CustomButton
к Button
нарушит управление, поскольку свойство Button.Command
не используется.
Вот почему классы, предназначенные для расширения, имеют виртуальные члены. Эти виртуальные члены отмечают точки входа или ключевые операции, определяющие поведение класса. Вы переопределяете эти виртуальные члены, чтобы изменить или расширить исходное базовое поведение. Button
выставляет таких виртуальных участников. Вы всегда должны сначала использовать и переопределять их, прежде чем думать о введении нового API, который изменит использование расширенного класса.
@BionicCode большое спасибо за ваш отзыв и подробное объяснение. Это очень ценится!
Я бы полностью избегал Interaction.Triggers. Вместо этого я бы привязал IsPressed к свойству модели представления, используя это решение, и вызвал бы требуемый метод команды в установщике свойств.