Data Trigger binding another element in different visual tree node

260 Views Asked by At

How to modify style of one element in a visual tree, based on style in triggers in another element in different visual tree node

For example,I am having a list of colors,

        ColorList = new List<ColorViewModel>();
        ColorList.Add(new ColorViewModel() { ColorCode = "#FF0000", ColorName="Red" });
        ColorList.Add(new ColorViewModel() { ColorCode = "#00FF00", ColorName="Green" });
        ColorList.Add(new ColorViewModel() { ColorCode = "#0000FF", ColorName="Blue" });
        this.DataContext = this;

I have the colors show in a ItemsControl and their name in another ItemsControl, When I hover on their name, I want to increase the size of color box for the corresponding color.

I tried setting the triggers based on element name, but since the scope is different. The following is the sample code, that covers my complex scenario. Is there a xaml way to overcome this? Any help appreciated.

<StackPanel Orientation="Horizontal">
    <ItemsControl ItemsSource="{Binding ColorList}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Rectangle Width="20" Height="20" Fill="{Binding ColorCode}">
                    <Rectangle.Style>
                        <Style TargetType="Rectangle">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ElementName=ColorName, Path=IsMouseOver}" Value="True">
                                    <Setter Property="Width" Value="30"/>
                                    <Setter Property="Height" Value="30"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Rectangle.Style>
                </Rectangle>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
    <ItemsControl ItemsSource="{Binding ColorList}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock x:Name="ColorName" Text="{Binding ColorName}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>
1

There are 1 best solutions below

0
BionicCode On

It would be possible using XAML only, if UIElement.IsMouseOver would have a setter. Since it is read-only it can't be target of a Binding. That IsMouseOver is read-only makes perfect sense, as it is intended to be set internally solely on mouse input.

Because of this, it is required to either extend ListBox (or ItemsControl) or to implement an attached behavior.

In order to transport the IsMouseOver flag information between both controls, you can add a dedicated property to the data model. The data model (or DataContext) is the only link between both item controls.

In the following example, this property is expected to be of the following definition:

private bool isPreviewEnabled;
public bool IsPreviewEnabled
{
  get => this.isPreviewEnabled;
  set
  {
    this.isPreviewEnabled = value;
    OnPropertyChanged(nameof(this.IsPreviewEnabled));
  }
}

Note that the model should implement INotifyPropertyChanged.

The following example is an attached behavior, that delegates the mouse over flag by providing an attached property as binding target.

Element.cs

public class Element : DependencyObject
{
  #region IsMouseOver attached property

  public static readonly DependencyProperty IsMouseOverElementProperty = DependencyProperty.RegisterAttached(
    "IsMouseOver",
    typeof(bool?),
    typeof(Element),
    new FrameworkPropertyMetadata(
      default(bool?), 
      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
      OnBindngAttached));

  public static void SetIsMouseOver(DependencyObject attachingElement, bool? value) => attachingElement.SetValue(Element.IsMouseOverProperty, value);

  public static bool? GetIsMouseOver(DependencyObject attachingElement) => (bool) attachingElement.GetValue(Element.IsMouseOverProperty);

  #endregion

  private static void OnBindngAttached(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    // Only listen to mouse events when the binding initializes the property.
    // This guarantees single subscription.
    if (d is FrameworkElement frameworkElement && e.OldValue == null)
    {
      frameworkElement.MouseEnter += DelegateIsMouseEnter;
      frameworkElement.MouseLeave += DelegateIsMouseLeave;
    }
  }

  private static void DelegateIsMouseEnter(object sender, MouseEventArgs e)
  {
    var attachedElement = sender as DependencyObject;
    SetIsMouseOver(attachedElement, true);
  }

  private static void DelegateIsMouseLeave(object sender, MouseEventArgs e)
  {
    var attachedElement = sender as DependencyObject;
    SetIsMouseOver(attachedElement, false);
  }
}

MainWindow.xaml

<StackPanel Orientation="Horizontal">
  <ItemsControl ItemsSource="{Binding ColorList}">
    <ItemsControl.ItemTemplate>
      <DataTemplate">
        <Rectangle Fill="{Binding ColorCode}">
          <Rectangle.Style>
            <Style TargetType="Rectangle">
              <Setter Property="Width" Value="20" />
              <Setter Property="Height" Value="20" />
              <Style.Triggers>
                <DataTrigger Binding="{Binding IsPreviewEnabled}" Value="True">
                  <Setter Property="Width" Value="30" />
                  <Setter Property="Height" Value="30" />
                </DataTrigger>
              </Style.Triggers>
            </Style>
          </Rectangle.Style>
        </Rectangle>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>

  <ItemsControl ItemsSource="{Binding ColorList}">
    <ItemsControl.ItemContainerStyle>
      <Style TargetType="ContentPresenter">

        <!-- 
          Delegate the IsMouseOver to the data model, 
          which is the data context of the item container 
        -->
        <Setter Property="Element.IsMouseOver" 
                Value="{Binding IsPreviewEnabled}" />
      </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding ColorName}" />
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</StackPanel>