Существует что-то фундаментальное о VisualStateGroup
s, что я не понимаю. Все, что я считал, привело меня полагать, что они являются ортогональными. Таким образом, изменение состояния в одной группе не будет влиять на другие группы. Действительно, они были бы довольно бессмысленны, если бы это не имело место.
Однако в попытке понять некоторое нечетное поведение я встречался, я соединил простой пример, который показывает, что изменение состояния в одной группе может инициировать анимацию в другом. Я пытаюсь понять, как это может быть.
Все, что я хочу, является a ToggleButton
- основанное управление, чтобы иметь одно появление при переключении (IsChecked == true
) независимо от того, фокусируется ли это или является ли мышь по нему, например.
Во-первых, я имею очень простой контроль что переходы между пользовательскими состояниями в пользовательской группе:
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
[TemplateVisualState(Name = normalState, GroupName = activationGroupName)]
[TemplateVisualState(Name = hoverState, GroupName = activationGroupName)]
[TemplateVisualState(Name = activatedState, GroupName = activationGroupName)]
public class CustomControl : ToggleButton
{
private const string activationGroupName = "Activation";
private const string normalState = "Normal";
private const string hoverState = "Hover";
private const string activatedState = "Activated";
public CustomControl()
{
this.DefaultStyleKey = typeof(CustomControl);
this.Checked += delegate
{
this.UpdateStates();
};
this.Unchecked += delegate
{
this.UpdateStates();
};
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.UpdateStates();
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
this.UpdateStates();
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
this.UpdateStates();
}
private void UpdateStates()
{
var state = normalState;
if (this.IsChecked.HasValue && this.IsChecked.Value)
{
state = activatedState;
}
else if (this.IsMouseOver)
{
state = hoverState;
}
VisualStateManager.GoToState(this, state, true);
}
}
Во-вторых, у меня есть шаблон для этого управления, которое изменяет цвет фона на основе текущего состояния:
<Style TargetType="local:CustomControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CustomControl">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Activation">
<VisualStateGroup.Transitions>
<VisualTransition To="Normal" GeneratedDuration="00:00:0.2"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal"/>
<VisualState x:Name="Hover">
<Storyboard>
<ColorAnimation Duration="00:00:0.2" To="Red" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Activated">
<Storyboard>
<ColorAnimation Duration="00:00:0.2" To="Blue" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="grid" Background="White">
<TextBlock>ToggleButton that should be white normally, red on hover, and blue when checked</TextBlock>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
По большей части это работает, но когда управление проигрывает, фокусируют его переходы назад к нормальному состоянию.
Я могу обойти это путем явной обработки изменений фокуса в моем управлении и предписания обновления состояния:
public CustomControl()
{
this.DefaultStyleKey = typeof(CustomControl);
this.Checked += delegate
{
this.UpdateStates();
};
this.Unchecked += delegate
{
this.UpdateStates();
};
// explicitly handle focus changes
this.GotFocus += delegate
{
this.UpdateStates();
};
this.LostFocus += delegate
{
this.UpdateStates();
};
}
Однако это не имеет никакого смысла мне, почему я должен сделать это.
Почему изменение состояния в одном VisualStateGroup
порождение анимации, определенной другим для выполнения? И каков самый простой, самый корректный способ для меня достигнуть моей установленной цели?
Спасибо
Обратите внимание, как метод GotoState
просто принимает имя состояния без каких-либо уточнений, к какой группе оно принадлежит. В то время как группы разделяют набор состояний, в которых может находиться элемент управления в любой момент времени (одно состояние на группу), имена состояний должны быть уникальными для всех состояний в менеджере.
Обратите также внимание, что ToggleButton
, от которого вы произошли, имеет следующий атрибут, связанный с классом:
[TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")]
Это указывает на то, что унаследованная вами логика в какой-то момент вызовет GotoState
с состоянием "Normal". Обычно элементы управления имеют только один метод UpdateStates
(как у вас), и в любой момент, когда любое из состояний может измениться, будет вызван этот метод. Эта функция, в свою очередь, вызовет GotoState
несколько раз с одним именем состояния из каждой группы. Таким образом, в момент, когда элемент управления теряет фокус, ToggleButton
вызывает GotoState
для установки состояния "Unfocused", но вместе с этим будет вызывать и "Normal" и либо "Checked", либо "Unchecked".
Теперь у вас есть собственный шаблон, в котором состояния "Unfocused" и "Checked" отсутствуют, поэтому вызовы GotoState
из ToggleButton
ничего не делают. Однако у вас есть "Normal". У VSM нет способа узнать, что ToggleButton
, использующий "Normal", предназначен для выбора из группы "CommonStates" шаблона по умолчанию. Вместо этого он просто находит VisualState
с именем "Normal", которое в данном случае находится в группе "Activiation". Следовательно, существующее состояние "Активировано" заменяется на "Нормальное".
Измените "Нормальный" на "Бла", и все будет работать так, как вы ожидаете.
(Это помогает помнить, что имена групп не делают многого, несмотря на их присутствие в TemplateVisualStateAttribute
, их единственное использование, о котором я могу думать, это в Blend UI)