Свойство Значение Анимация, запускаемая пользовательским DependencyProperty в UserControl?

Вопрос прелюдии:

Как я могу оживить Angle Собственность RotateTransform из UIElement A когда значение Custom DependencyProperty типа логического становится True когда я нажимаю на UIElement B все внутри UserControl? И в XAML ТОЛЬКО (или в основном)? если возможно:)

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


Контекст:

Речь идет о триггерах анимации и привязке пользовательских свойств в одном элементе UserControl. Пока не задействовано окно.

Для начала предположим, что я создал UserControl, который имеет основной Grid который содержит два других Grids, Самые простые схемы:

<!-- MyControl.xaml -->
<UserControl ...blahblahblah>
    <Grid>
        <Grid x:Name="TiltingGrid">
            <!-- This Grid contains UIElements that I want to tilt alltogether -->
        </Grid>
        <Grid>
            <Ellipse x:Name="TiltingTrigger" ...blahblahblah>
                <!-- This Ellipse is my "click-able" area -->
            </Ellipse>
        </Grid>
    </Grid>
</UserControl>

Затем в коде у меня есть DependencyProperty называется IsTilting,

// MyControl.xaml.cs
public bool IsTilting
{
    // Default value is : false
    get { return (bool)this.GetValue(IsTiltingProperty); }
    set { this.SetValue(IsTiltingProperty, value); }
}

private static readonly DependencyProperty IsTiltingProperty =
    DependencyProperty.Register(
        "IsTilting",
        typeof(bool),
        typeof(MyControl),
        new FrameworkPropertyMetadata(
            false,
            new PropertyChangedCallback(OnIsTiltingPropertyChanged)));

private static void OnIsTiltingPropertyChanged(...) { ... }
    // .. is a classic Callback which calls
    // private void OnIsTiltingChanged((bool)e.NewValue)
    // and/or
    // protected virtual void OnIsTiltingChanged(e) ...

Затем я определил некоторые свойства для моей сетки с именем TiltingGrid в XAML:

<Grid x:Name="TiltingGrid"
    RenderTransformOrigin="0.3, 0.5">

    <Grid.RenderTransform>
        <RotateTransform 
            x:Name="TiltRotate" Angle="0.0" />
            <!-- Angle is the Property I want to animate... -->
    </Grid.RenderTransform>

    <!-- This Grid contains UIElements -->
    <Path ... />
    <Path ... />
    <Ellipse ... />
</Grid>

И я хотел бы вызвать наклон при нажатии на определенную область внутри этого UserControl: Ellipse, в secund Grid:

<Grid>
    <Ellipse x:Name="TiltingTrigger"
        ... Fill and Stroke goes here ...
        MouseLeftButtonDown="TryTilt_MouseLeftButtonDown"
        MouseLeftButtonUp="TryTilt_MouseLeftButtonUp">
    </Ellipse>
</Grid>

Если я не ошибаюсь, Ellipse не имеет события Click, поэтому мне пришлось создать два EventHandlers для MouseLeftButtonDown а также MouseLeftButtonUp, Я должен был сделать это таким образом, чтобы иметь возможность:

  • Заставьте Ellipse перехватывать Mouse при MouseLeftButtonDown и установите для частного поля значение true
  • Проверьте, находится ли Mouse Point внутри эллипса при MouseLeftButtonUp, установите значение частного поля равным false, затем отпустите мышь.
  • Инвертировать значение свойства DependencyProperty IsTilting (true / false), если происходит что-то похожее на "щелчок" (.. который вызовет анимацию наклона, если я смогу разрешить соответствующую привязку..)

Я сохраню вам код MouseLeftDown/Up, но могу предоставить его при необходимости. Что они делают, так это меняют значение DP.


Вопросы):

Я не знаю, как запустить анимацию угла при обновлении моего DependencyProperty. Что ж. Это не актуальная проблема, это недостаток знаний, которые я считаю:

  • Я не знаю, как захватить пользовательское событие для использования с <EventTrigger>
  • Я не знаю, как и где вызвать StoryBoard, используя True/False DependencyProperty.

И актуальный вопрос:

Отныне, как мне объявить код, который делает Angle Собственность RotateTransform оживить от 0.0 в 45.0 (Рендеринг трансформации моей сетки " TiltingGrid когда мой ДП IsTilting установлен в true и оживить обратно 0.0 когда это False?

в основном в стиле XAML..?

У меня есть рабочий код в коде C# (подробно ниже). То, что я ищу, - это работоспособное решение в XAML (потому что обычно очень легко переписать почти все в Code Behind, когда вы знаете, как это сделать в XAML)


Что я пробовал до сих пор...

Отныне вам не нужно читать дальше, если вы абсолютно не хотите знать все детали...


1) Запуск анимации с использованием встроенного Ellipse EventTriggers работает только для событий, определенных для этого конкретного UIElement (Enter / Leave / MouseLeftDown...). Выполнено много раз с помощью UIElement.

Но эти триггеры не те, которые мне нужны: моя сетка должна наклоняться в зависимости от On/Off или же True/False пользовательское состояние в DP, а не когда происходит что-то подобное мышиному действию.

<Ellipse.Triggers>
    <EventTrigger RoutedEvent="UIElement.MouseEnter">
        <BeginStoryboard>
            <Storyboard>
                <DoubleAnimation 
                    Storyboard.TargetName="TiltRotate" 
                    Storyboard.TargetProperty="Angle" 
                    From="0.0" To="45.0" 
                    Duration="0:0:0.2" />
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
    <EventTrigger RoutedEvent="UIElement.MouseLeave">
    ...
</Ellipse.Triggers>

Когда мышь входит в эллипс, моя Сетка соответственно наклоняется, но, следовательно, как я могу получить доступ к пользовательским событиям, определенным в моем UserControl?


2) Затем, исходя из приведенной выше схемы, я предположил, что мне просто нужно было создать Routed Event на моем MyControl Класс или два, на самом деле:

  • TiltingActivated
  • TiltingDisabled

,

public static readonly RoutedEvent TiltingActivatedEvent = 
    EventManager.RegisterRoutedEvent(
        "TiltingActivated",
        RoutingStrategy.Bubble, 
        typeof(RoutedEventHandler), 
        typeof(EventHandler));

public event RoutedEventHandler TiltingActivated
{
    add { AddHandler(MyControl.TiltingActivatedEvent, value); }
    remove { RemoveHandler(MyControl.TiltingActivatedEvent, value); }
}

private void RaiseTiltingActivatedEvent()
{
    RoutedEventArgs newEventArgs = 
        new RoutedEventArgs(MyControl.TiltingActivatedEvent, this);
    RaiseEvent(newEventArgs);
}

Тогда я звоню RaiseTiltingActivatedEvent() в одном методе, вызванном моим IsTilting DependencyProperty Callback, когда его новое значение true, а также RaiseTiltingDisabledEvent() когда его новое значение false,

Замечания: IsTilting Значение изменяется на true или false при "щелчке" эллипса, и два события запускаются соответственно. Но есть проблема: это не Эллипс, который запускает События, а UserControl сам.

Во всяком случае, я пытался заменить <EventTrigger RoutedEvent="UIElement.MouseEnter"> со следующими:

Попытка одна:

<EventTrigger RoutedEvent="
    {Binding ic:MyControl.TiltingActivated, 
    ElementName=ThisUserControl}">

.. и я получаю:

"System.Windows.Markup.XamlParseException: (...)"
"A 'Binding' can only be set on a DependencyProperty of a DependencyObject."

Я предполагаю, что не могу привязаться к событию?

Попытка два:

<EventTrigger RoutedEvent="ic:MyControl.TiltingActivated">

.. и я получаю:

"System.NotSupportedException:"
"cannot convert RoutedEventConverter from system.string"

Я предполагаю, что имя RoutedEvent не может быть разрешено? В любом случае, этот подход заставляет меня отходить от моей первоначальной цели: запускать DoubleAnimation при изменении пользовательского свойства (поскольку в более сложных сценариях было бы проще запускать различные анимации и вызывать конкретные методы, все в Code Behind, когда мы можем иметь десятки разных ценностей, чем создание длинных и хитрых вещей на XAML? Лучше всего научиться делать и то и другое. Я очень хочу знать)


3) Затем я наткнулся на эту статью: WPF Animation Tutorial для начинающих.

Код позади создания анимации. Это то, чему я хотел научиться, узнав, как это сделать в XAML. В любом случае, давайте попробуем.

a) Создайте два свойства анимации (личное), одно для наклона анимации, а другое для наклона анимации назад.

private DoubleAnimation p_TiltingPlay = null;
private DoubleAnimation TiltingPlay
{
    get {
        if (p_TiltingPlay == null) {
            p_TiltingPlay = 
                new DoubleAnimation(
                    0.0, 45.0, new Duration(TimeSpan.FromSeconds(0.2)));
        }
        return p_TiltingPlay;
    }
}
// Similar thing for TiltingReverse Property...

б) Подпишитесь на два события, а затем установите Angle Animation нашего RotateTransform во время выполнения в коде позади:

private void MyControl_TiltingActivated(object source, EventArgs e)
{
    TiltRotate.BeginAnimation(
        RotateTransform.AngleProperty, TiltingPlay);
}

// Same thing for MyControl_TiltingDisabled(...)

// Subscribe to the two events in constructor...
public MyControl()
{
    InitializeComponent();
    this.TiltingActivated += 
        new RoutedEventHandler(MyControl_TiltingActivated);
    this.TiltingDisabled += 
        new RoutedEventHandler(MyControl_TiltingDisabled);
}

В основном, когда я "щелкаю" (MouseButtonLeftDown + Up) на эллипсе:

  • Мышь попала в точку
  • если в пределах области эллипса, измените DP IsTilting на Not IsTilting.
  • Затем IsTilting запускает либо TiltingActivation или TiltingDisabled.
  • Оба они захвачены, а затем активируется соответствующая анимация наклона (частные свойства) <RotateTransform ..> сетки.

И это работает!!!

Я сказал, что это будет очень просто в коде! (длинный код... да, но это работает) Надеюсь, с шаблонами фрагментов это не так скучно.


Но я до сих пор не знаю, как это сделать в XAML.: /

4) Так как мои пользовательские события, похоже, выходят за рамки XAML, как насчет <Style> ? Обычно связывание в Style это как дышать. Но, честно говоря, я не знаю, с чего начать.

  • целью анимации является Angle Собственность <RotateTransform /> применяется к Grid,
  • переплетенный Деп. Свойство IsTilting является пользовательским DP MyControl не UserControl,
  • и один Ellipse управляет обновлением DP.

давайте попробуем что-то вроде <RotateTransform.Style>

<RotateTransform ...>
    <RotateTransform.st...>
</RotateTransform>
<!-- such thing does not exists -->

или же RotateTransform.Triggers? ... тоже не существует

ОБНОВЛЕНИЕ: Этот подход работает, объявляя Стиль в Сетка, чтобы оживить, как объяснено в ответе Клеменса. Чтобы разрешить привязку пользовательского свойства UserControl, мне просто нужно было использовать RelativeSource={RelativeSource AncestorType=UserControl}}, И чтобы "нацелить" свойство угла в RotateTransform, мне просто нужно было использовать RenderTransform.Angle,


Что-то еще?

  1. Я часто вижу образцы, которые устанавливают DataContext к чему-то вроде "я". Я не совсем понимаю, что такое DataContext, но я предполагаю, что он по умолчанию указывает все точки разрешения пути на объявленный класс для привязок. Я уже использовал это в одном UserControl, который решил мою проблему, но я не копал глубже, чтобы понять, как и почему. Возможно, это поможет разрешить захват пользовательских событий в коде непосредственно со стороны XAML?

  2. Один XAML в основном способ, которым я почти уверен, будет работать:

    • создать пользовательский UserControl для этого эллипса, скажем, EllipseButton, со своими событиями и свойствами
    • затем вставьте это в MyControl UserControl.
    • Захватите событие TiltingActivation элемента EllipseButton, чтобы запустить DoubleAnimation в раскадровке EllipseButton, точно так же, как это можно сделать для события Click кнопки.

    Это бы хорошо работало, но я нахожу хакерским создание и встраивание другого элемента управления, чтобы иметь возможность доступа к соответствующему пользовательскому событию. MyControl - это не проект SuperWonderfulMegaTop, который потребовал бы такой операции. Я уверен, что упускаю что-то оооочень очевидное; не могу поверить в то, что просто вне мира WPF, в WPF не может быть даже проще.

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

  3. Возможно определяющий <Grid.Style> или что-то подобное... но я не знаю как. Я знаю только как пользоваться <Setter>, Я не знаю, как создать EventTriggers в объявлении стиля. ОБНОВЛЕНИЕ: объяснено ответом Клеменса.

  4. Этот SO вопрос (триггер Fire в UserControl на основе DependencyProperty) предлагает создать стиль в UserControl.Resources, Попробовал следующее... Это не работает (и там нет анимации в любом случае - я пока не знаю, как объявить анимацию в стиле)

,

<Style TargetType="RotateTransform">
    <Style.Triggers>
        <DataTrigger
            Binding="{Binding IsTilting, ElementName=ThisUserControl}" Value="True">
            <Setter Property="Angle" Value="45.0" />
        </DataTrigger>
    </Style.Triggers>
</Style>
  1. Этот SO вопрос (привязка к углу RotateTransform в DataTemplate не вступает в силу) имеет много неизвестных мне знаний, чтобы быть понятным. Однако, предполагая, что предложенный обходной путь работает, я нигде не вижу ничего похожего на анимацию. Просто привязка к значению, которое не анимировано. Я не думаю, что Угол оживляет себя волшебным образом.

  2. В Code Behind, как рабочий код выше, я мог бы создать еще один DependencyProperty с именем GridAngle (двойной), затем привязать свойство угла RotateTransform к этому новому DP, а затем анимировать этот DP напрямую??? Стоит попробовать, но позже: я устал.

  3. Я только что обнаружил, что мои зарегистрированные события имеют Bubble Strategy. Это будет иметь значение, если событие будет захвачено некоторыми родительскими контейнерами, но я хочу обрабатывать все непосредственно внутри UserControl, а не как в этом вопросе SO. Однако стратегия туннелирования - которую я пока не понимаю - может сыграть свою роль: позволит ли туннелирование моему эллипсу захватывать события моего UserControl? Приходится читать документацию снова и снова, потому что она все еще очень неясна для меня... Что меня беспокоит, так это то, что я все еще не могу использовать свои пользовательские события в этом UserControl:/

  4. Как насчет CommandBinding? Это кажется очень интересным, но это совершенно другая глава для изучения. Кажется, это связано с большим количеством кода позади, и так как у меня уже есть рабочий код (который выглядит более читабельным для меня...)

  5. В этом вопросе SO (триггеры данных WPF и доски рассказов) принятый ответ, по-видимому, работает, только если я анимирую свойство элемента пользовательского интерфейса, который может иметь UIElement.Style определение. RotateTransform не имеет такой способности.

    Другой ответ предполагает использование ContentControl, ControlTemplate... Как и в случае с CommandBinding выше, я недостаточно глубоко копал, чтобы понять, как я могу адаптировать это к своему UserControl.

    Тем не менее, эти ответы, кажется, в основном соответствуют моим потребностям, особенно в способе ContentControl. Позже у меня будет несколько попыток, и я посмотрю, решает ли это XAML главным образом для реализации желаемого поведения.:)

  6. И наконец, этот ТАК вопрос (EventTrigger связывается с событием из DataContext) предлагает использовать Blend/Interactivity, Подход выглядит неплохо, но у меня нет Blend SDK, и я не очень хочу этого делать, если мне абсолютно не нужно... Снова: еще одна целая глава, чтобы поесть...


Примечание:

Как вы уже догадались, я новичок в WPF/XAML (я знаю, что это не оправдание), который я начал изучать несколько недель назад. Я вроде "все это было бы очень легко сделать в WinForms прямо сейчас...", но, возможно, вы могли бы помочь мне понять, насколько легко было бы добиться этого в WPF:)

Я много искал (знаю, что это тоже не оправдание), но мне не повезло на этот раз. - Ладно, я только что прочитал три десятка статей, проектов кода и тем SO, а также документацию MSDN о триггерах, анимациях, перенаправленных событиях... кажется, просто полирует поверхность, не углубляясь в ядро ​​(кажется, что MS думает, что наследуется от Баттона есть способ решить практически все...)

1 ответ

Решение

Длинный вопрос, короткий ответ. Используйте визуальные состояния:

<UserControl ...>
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState x:Name="TiltedState">
                    <Storyboard>
                        <DoubleAnimation
                            Storyboard.TargetName="TiltingGrid"
                            Storyboard.TargetProperty="RenderTransform.Angle"
                            To="45" Duration="0:0:0.2"/>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
            <Grid.RenderTransform>
                <RotateTransform/>
            </Grid.RenderTransform>
            ...
        </Grid>
    </Grid>
</UserControl>

Всякий раз, когда выполняется соответствующее условие, звоните

VisualStateManager.GoToState(this, "TiltedState", true);

в коде UserControl позади. Это, конечно, также может быть вызвано в PropertyChangedCallback свойства зависимостей.


Без использования визуальных состояний вы можете создать стиль для вашей TiltingGrid, который использует DataTrigger с привязкой к вашему UserControl's IsTilted имущество:

<Grid x:Name="TiltingGrid" RenderTransformOrigin="0.3, 0.5">
    <Grid.Style>
        <Style TargetType="Grid">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsTilted,
                    RelativeSource={RelativeSource AncestorType=UserControl}}"
                    Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Angle"
                                    To="45" Duration="0:0:0.2"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetProperty="RenderTransform.Angle"
                                    To="0" Duration="0:0:0.2"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.ExitActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Grid.Style>
    <Grid.RenderTransform>
        <RotateTransform/>
    </Grid.RenderTransform>
    ...
</Grid>
Другие вопросы по тегам