Title

Saturday, 7 February 2015

Why does changing an Image source through a trigger in XAML not fire the SourcePropertyChanged method of my custom control?


Here is the control template:

<Style TargetType="{x:Type ui:NavigationListViewItem}">   <Setter Property="Foreground" Value="White" />   <Setter Property="Template">   <Setter.Value>   <ControlTemplate TargetType="{x:Type ui:NavigationListViewItem}">   <StackPanel x:Name="itemPanel"   MaxWidth="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ui:NavigationListView}}, Path=ActualWidth}"   VerticalAlignment="Center" >   <Grid Margin="20,10">   <Grid.ColumnDefinitions>   <ColumnDefinition Width="19" />   <ColumnDefinition Width="*" />   </Grid.ColumnDefinitions>   <Grid.RowDefinitions>   <RowDefinition MinHeight="22" />   </Grid.RowDefinitions>     <ui:ColorableImage Grid.Column="0" Grid.Row="0"   x:Name="itemImage"   Source="{Binding Path=DefaultImage, RelativeSource={RelativeSource TemplatedParent}}"   Height="14"   Width="16"   VerticalAlignment="Center"   HorizontalAlignment="Right"   Margin="0,0,3,0"   Color="BlanchedAlmond"/>   <TextBlock Grid.Column="1" Grid.Row="0"   x:Name="itemText"   FontSize="14"   FontFamily="Segoe UI"   FontWeight="Bold"   TextWrapping="Wrap"   VerticalAlignment="Center"   HorizontalAlignment="Left"   Text="{Binding Path=Text, RelativeSource={RelativeSource TemplatedParent}}">   </TextBlock>   </Grid>   </StackPanel>     <ControlTemplate.Triggers>   <Trigger Property="IsSelected" Value="True">   <Setter TargetName="itemText" Property="Foreground" Value="#565759" />   <Setter TargetName="itemImage" Property="Color" Value="Blue" />   <Setter TargetName="itemPanel" Property="Background" Value="White" />   </Trigger>   <MultiTrigger>   <MultiTrigger.Conditions>   <Condition Property="IsMouseOver" Value="True" />   <Condition Property="IsSelected" Value="False" />   </MultiTrigger.Conditions>   <Setter TargetName="itemText" Property="Foreground" Value="{StaticResource LightBlueColorBrush}" />   <Setter TargetName="itemPanel" Property="Background" Value="#EEEEEE" />   <Setter TargetName="itemImage" Property="Color" Value="Brown"/>   <Setter Property="DefaultImage" Value="test.png" />   </MultiTrigger>   </ControlTemplate.Triggers>   </ControlTemplate>   </Setter.Value>   </Setter>  </Style>

and my class (you don't really need to look at the whole thing, I think, but I've included everything for completeness' sake):

public class ColorableImage : Image {   // Used by the converter to color the source (original image is preserved through the below 2 properties)   public static readonly DependencyProperty ColorProperty;   // Used for the OriginalSource property, because if it is used for binding, the original image will be lost; this is the public, accessible source property   public static readonly DependencyProperty OriginalSourceProperty;   // Used for the color binding, because a control cannot bind to itself without going into an infinite loop; this is the private image property for preservation   public static readonly DependencyProperty OriginalImageProperty;     static ColorableImage()   {   DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorableImage), new FrameworkPropertyMetadata(typeof(ColorableImage)));   SourceProperty.OverrideMetadata(typeof(ColorableImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(SourcePropertyChanged)));     ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorableImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(ColorPropertyChanged)));   OriginalSourceProperty = DependencyProperty.Register("OriginalSource", typeof(ImageSource), typeof(ColorableImage), new UIPropertyMetadata(null));   OriginalImageProperty = DependencyProperty.Register("OriginalImage", typeof(Image), typeof(ColorableImage), new UIPropertyMetadata(null));   }     public Color Color   {   get { return (Color)GetValue(ColorProperty); }   set { SetValue(ColorProperty, value); }   }     // Public property that is allowed to be accessed (but not set) through the XAML   public ImageSource OriginalSource   {   get { return (ImageSource)GetValue(OriginalSourceProperty); }   protected set { SetValue(OriginalSourceProperty, value); }   }     // Private property that uses OriginalSource; this extra property is necessary because you cannot bind to an ImageSource property; you must bind to the source of an Image property   private Image OriginalImage   {   get { return (Image)GetValue(OriginalImageProperty); }   set { SetValue(OriginalImageProperty, value); }   }     private static void SourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)   {   // Note that this method does not get called when the Source property is set to another value through the XAML   // Whenever the Source is changed, set the original source/image to the new image before the new image gets colored   ColorableImage castedSender = (ColorableImage)sender;   if (castedSender.OriginalImage == null)   {   castedSender.OriginalSource = castedSender.Source;   castedSender.OriginalImage = new Image();   castedSender.OriginalImage.Source = castedSender.Source;   }   }     private static void ColorPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)   {   ColorableImage castedSender = (ColorableImage)sender;   Binding binding = new Binding("Source")   {   Source = castedSender.OriginalImage,   Converter = new HighlightImageConverter(),   ConverterParameter = castedSender.Color   };   castedSender.SetBinding(ColorableImage.SourceProperty, binding);   }     public event PropertyChangedEventHandler PropertyChanged;   protected void NotiftyPropertyChanged(string name)   {   if (PropertyChanged != null)   {   PropertyChanged(this, new PropertyChangedEventArgs(name));   }   }  }

What I am completely confused at is that, using breakpoints in my code, I find that:

  1. SourcePropertyChanged is indeed fired when the Source is set when the ColorableImage is defined in the XAML
  2. ColorPropertyChanged is called both when the ColorableImage is defined in the XAML and when the trigger occurs (like it's supposed to do)
  3. SourcePropertyChanged is not called when the trigger occurs!

EDIT: Just in case (not sure what else to look at here), here are the small classes for the list:

public class NavigationListView : ListView  {   static NavigationListView()   {   DefaultStyleKeyProperty.OverrideMetadata(typeof(NavigationListView), new FrameworkPropertyMetadata(typeof(NavigationListView)));   }     protected override DependencyObject GetContainerForItemOverride()   {   return new NavigationListViewItem();   }    }

.

public class NavigationListViewItem : ListViewItem  {   public static readonly DependencyProperty DefaultImageProperty;   public static readonly DependencyProperty TextProperty;     static NavigationListViewItem()   {   DefaultStyleKeyProperty.OverrideMetadata(typeof(NavigationListViewItem), new FrameworkPropertyMetadata(typeof(NavigationListViewItem)));   DefaultImageProperty = DependencyProperty.Register("DefaultImage", typeof(ImageSource), typeof(NavigationListViewItem), new UIPropertyMetadata(null));   TextProperty = DependencyProperty.Register("TextProperty", typeof(string), typeof(NavigationListViewItem), new UIPropertyMetadata(null));   }     public ImageSource DefaultImage   {   get { return (ImageSource)GetValue(DefaultImageProperty); }   set   {   SetValue(DefaultImageProperty, value);   }   }     public string Text   {   get { return (string)GetValue(TextProperty); }   set { SetValue(TextProperty, value); }   }  }
Answer

You aren't showing the spot in XAML where you're applying this Style to a control, but I assume on the NavigationListViewItem instances you're setting a value for DefaultImage. In the precedence order for setting a DependencyProperty triggers in control templates fall below local values so your MultiTrigger is likely not changing the value on the parent control. Inside the template you should almost always set values directly on the elements inside the template rather than updating parent values that you've bound to inside the template.

No comments:

Post a Comment