Reputation: 3520
Suppose I have a Window
or UserControl
with a boatload of named elements. I want to change all these elements' property values based on a single property of either my view model or a custom DP on the parent (really doesn't matter which, because I can easily bind the DP to the view model property).
Here is a barebones example:
<Window x:Class="TriggerFun.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:TriggerFun"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
using System;
using System.ComponentModel;
using System.Windows;
namespace TriggerFun
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ViewModel ViewModel => this.DataContext as ViewModel;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.AlternativeLayout = !this.ViewModel.AlternativeLayout;
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
bool _alternativeLayout;
public bool AlternativeLayout
{
get => _alternativeLayout;
set
{
_alternativeLayout = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AlternativeLayout)));
}
}
}
}
What I would like to do in this example is swap the columns of the red and yellow Rectangles
when the user clicks the button. (And yes of course I know I can do this in code-behind. I want to do this in pure XAML).
What would make eminent sense to me is if I could do add this to Window
:
<Window.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Window.Triggers>
Well that doesn't work because I get a runtime error, Triggers collection members must be of type EventTrigger.
So all triggers that are children of anything other than a Style
, DataTemplate
, or ControlTemplate
I guess have to be EventTrigger
s? Fine.
So then I try this:
<Window.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
That won't even compile: TargetName property cannot be set on a Style Setter.
I know I can use TargetName
in DataTemplate
or ControlTemplate
Triggers, but when defining UserControl
's and Window
s of course you usually don't set the DataTemplate
but rather just set the child content directly.
The only thing I can do that I know works is take each element that I want to be changed and give it its own inline style with triggers, with the final XAML looking incredibly ugly:
<Rectangle x:Name="Rect1"
Width="200" Height="200"
Fill="Red">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="1"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Rectangle x:Name="Rect2"
Width="200" Height="200"
Fill="Yellow">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
Obviously this doesn't scale particularly well.
This is SO easy to do with ControlTemplate
s and DataTemplate
s but seems next to impossible when making UserControl
s. Is there something I'm missing?
Upvotes: 0
Views: 1501
Reputation: 7918
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
The application of your AttachedProperty can be simplified somewhat.
Let's change the implementation a bit:
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
{
#region ContentTriggerCollection Triggers dependency property
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
"ShadowTriggers",
typeof(ContentTriggerCollection),
typeof(ContentTrigger),
new FrameworkPropertyMetadata(
null,
OnTriggersChanged));
public static ContentTriggerCollection GetTriggers(DependencyObject obj)
{
var value = (ContentTriggerCollection)obj.GetValue(TriggersProperty);
if (value == null)
SetTriggers(obj, value = new ContentTriggerCollection());
return value;
}
public static void SetTriggers(DependencyObject obj, ContentTriggerCollection value)
{
obj.SetValue(TriggersProperty, value);
}
private static void OnTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.OldValue is ContentTriggerCollection oldTriggers)
oldTriggers.SetApply(null);
if (args.NewValue is ContentTriggerCollection newTriggers)
newTriggers.SetApply(obj);
}
#endregion
public BindingBase Binding { get; set; }
public object Value { get; set; }
public SetterBaseCollection Setters { get; } = new SetterBaseCollection();
#region object ActualValue dependency property
internal static readonly DependencyProperty ActualValueProperty = DependencyProperty.Register(
"ActualValue",
typeof(object),
typeof(ContentTrigger),
new PropertyMetadata(
(object)null,
(obj, args) =>
{
((ContentTrigger)obj).OnActualValueChanged(args);
}));
private void OnActualValueChanged(DependencyPropertyChangedEventArgs args)
{
if (TestIsTriggered(args.NewValue))
ExecuteTrigger();
else
RestoreValues();
}
#endregion
private bool TestIsTriggered(object newValue)
{
if (newValue is bool b)
return b && (this.Value as string == "True" || this.Value as string == "true") ||
!b && (this.Value as string == "False" || this.Value as string == "false");
else
return object.Equals(this.Value, newValue);
}
public void Apply(DependencyObject obj)
{
if (!(obj is FrameworkElement fe))
return;
_target = fe;
BindingOperations.SetBinding(this, DataContextProperty, new Binding
{
Source = fe,
Path = new PropertyPath(DataContextProperty)
});
BindingOperations.SetBinding(this, ActualValueProperty, this.Binding);
}
private void ExecuteTrigger()
{
if (_target == null || _isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null)
continue;
_originalValues[(targetElem, setter.Property)] = targetElem.GetValue(setter.Property);
targetElem.SetCurrentValue(setter.Property, ResolveSetterValue(setter));
}
_isTriggered = true;
}
private void RestoreValues()
{
if (_target == null || !_isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null ||
// Value changed some other way since trigger?
targetElem.GetValue(setter.Property) != ResolveSetterValue(setter))
continue;
object restoredValue;
if (_originalValues.TryGetValue((targetElem, setter.Property), out restoredValue))
{
targetElem.SetCurrentValue(setter.Property, restoredValue);
}
}
_isTriggered = false;
}
private FrameworkElement GetTargetElement(string name)
{
FrameworkElement targetElem;
if (!_targetElements.TryGetValue(name, out targetElem))
{
targetElem = _target.FindName(name) as FrameworkElement;
if (targetElem != null)
_targetElements[name] = targetElem;
}
return targetElem;
}
private object ResolveSetterValue(Setter setter)
{
if (setter.Value is DynamicResourceExtension dr)
return _target.FindResource(dr.ResourceKey);
return setter.Value;
}
private Dictionary<(FrameworkElement, DependencyProperty), object> _originalValues =
new Dictionary<(FrameworkElement, DependencyProperty), object>();
private Dictionary<string, FrameworkElement> _targetElements = new Dictionary<string, FrameworkElement>();
private bool _isTriggered = false;
private FrameworkElement _target;
}
public class ContentTriggerCollection : Collection<ContentTrigger>
{
public DependencyObject Apply { get; private set; }
public void SetApply(DependencyObject apply)
{
Apply = apply;
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(apply);
}
}
protected override void ClearItems()
{
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(null);
}
base.ClearItems();
}
protected override void InsertItem(int index, ContentTrigger item)
{
base.InsertItem(index, item);
if (item != null)
item.Apply(Apply);
}
protected override void RemoveItem(int index)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.RemoveItem(index);
}
protected override void SetItem(int index, ContentTrigger item)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.SetItem(index, item);
if (item != null)
item.Apply(Apply);
}
}
<local:ContentTrigger.Triggers>
<local:ContentTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect1" Property="Rectangle.Fill" Value="Green"/>
<Setter TargetName="Rect1" Property="Rectangle.Stroke" Value="Purple"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
<Setter TargetName="Rect2" Property="Rectangle.Fill" Value="Gray"/>
<Setter TargetName="Rect2" Property="Rectangle.Stroke" Value="Black"/>
</local:ContentTrigger>
</local:ContentTrigger.Triggers>
The key change is registering the name "ShadowTriggers", which is different from the name of the Get and Set methods. This registration does not allow XAML to refer to the AttachedProperty without going through the getter and setter.
Upvotes: 1
Reputation: 169250
You could define a ControlTemplate
for the window or user control and define the triggers in the template:
<Window.DataContext>
<local:Window23ViewModel />
</Window.DataContext>
<Grid>
<UserControl>
<UserControl.Template>
<ControlTemplate>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</ControlTemplate.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
</Grid>
Upvotes: 1