views:

151

answers:

3

I have a label control where I use a converter to switch its styles based on a bool property, IsCheckedOut on my viewmodel, like so:

    <UserControl.Resources>
      <Converters:BooleanStyleConverter 
       x:Key="BooleanStyleConverter " 
       StyleFalse="{StaticResource HeaderLabelStyle}" 
       StyleTrue="{StaticResource HeaderLabelHighlightedStyle}" />
     </UserControl.Resources>

    <Label Style="{Binding Path=IsCheckedOut, 
Converter={StaticResource BooleanStyleConverter}}">
    some content here
    </Label>

The converter simply returns one of the two styles:

public class BooleanStyleConverter : IValueConverter 
    {
        public Style StyleFalse { get; set; }
        public Style StyleTrue { get; set; }
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if ((bool)value)
            {
                return StyleTrue;
            }
            return StyleFalse;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

And the styles look something like this:

<Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
 <Setter Property="Template">
  <Setter.Value>
   <ControlTemplate TargetType="Label">
    <Border Background="{StaticResource RedGradient}">
     <ContentPresenter />
    </Border>
   </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

<Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
 <Setter Property="Template">
  <Setter.Value>
   <ControlTemplate TargetType="Label">
    <Border Background="{StaticResource BlueGradient}">
     <ContentPresenter />
    </Border>
   </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

So when IsCheckedOut is true the label gets a red background, and when it's false it gets a blue background (well, the styles are a bit more complicated, but you get the idea). Now, I'd like to have a transition between the styles, so that the new colors fade in when IsCheckedOut changes. Does anyone know how I can accomplish this?

A: 

Sorry, but you're doing it wrong.

You do get bonus points for being extremely creative and ambitious in your solution. But you've taken the proverbial 5kg hammer down on a thumbtack.

The correct solution in this situation is to use Storyboards nested in VSM States. It looks like you essentially have 2 States for your UI: One where some business logic value is true and another state for when it's false. Note that the aforementioned distinction is 100% technology independent. In any technology, whatever it is you're trying to achieve would be considered 2 states for your UI.

In Silverlight/WPF, instead of hacking together something that mimics UI states, you could actually create VisualStateManager states.

Technically it would work in the following way:
1. Your UserControl would have 1 VisualStateGroup that has 2 VisualStates (one for true and another for false).
2. Those VSM states each represent 1 storyboard.
3. That storyboard would change the template or any other properties you feel are appropriate.

To learn the basics of VSM I strongly suggest you spend the next 30 minutes watching the following VSM videos: http://expression.microsoft.com/en-us/cc643423.aspx (Under "How Do I?")
Seriously, these videos are phenomenally successful in explaining VSM. The one that most pertinent to your dilemma is "Add States to a Control" but I'll suggest you watch all of them.

In WPF, you could use the VisualStateManager from the WPF Toolkit.

JustinAngel
Thanks, Justin! At the moment, all the IsCheckedOut value should do is switch the color of that label, so I figured just binding the style somehow to that value would be the simplest way. But you're absolutely right - even though I didn't think of it as different states, that's exactly what it is. Thanks for the eye opener. :)
Zissou
A: 

Hi Zissou,

As Justin said, you are doing something wrong, and you might want to do five steps back and reconsider your approach...

But I really liked this puzzle :). I've solved it without VSM, just to demonstrate how flexible WPF is. Basic principle here is using Dependency properties value coercion. We track all style changes, but we don't let the new value go out from Coerce() function, until we complete transition animation.

To simplify your testing, just copy/paste the following code and check if it works for you :). If you want to get into details - feel free to ask additional questions.

XAML:

<Window x:Class="WpfApplication5.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:loc="clr-namespace:WpfApplication5"
        Title="Fade Styles"
        Width="320"
        Height="240">
  <Window.Resources>

    <SolidColorBrush x:Key="RedGradient" Color="Red" />
    <SolidColorBrush x:Key="BlueGradient" Color="Blue" />

    <Style x:Key="HeaderLabelStyle" TargetType="{x:Type Label}">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Label">
            <Border Background="{StaticResource RedGradient}">
              <ContentPresenter />
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <Style x:Key="HeaderLabelHighlightedStyle" TargetType="{x:Type Label}">
      <Setter Property="Foreground" Value="White" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Label">
            <Border Background="{StaticResource BlueGradient}">
              <ContentPresenter />
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <loc:BooleanStyleConverter x:Key="BooleanStyleConverter"
                                           StyleFalse="{StaticResource HeaderLabelStyle}"
                                           StyleTrue="{StaticResource HeaderLabelHighlightedStyle}" />
  </Window.Resources>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Label Style="{Binding IsChecked, ElementName=chkToggle, Converter={StaticResource BooleanStyleConverter}}"
           loc:StyleAnimation.IsActive="True"
           Content="Some content here" />
    <CheckBox Grid.Row="1" Name="chkToggle" Content="Use Blue" />
  </Grid>
</Window>

Take a look here on loc:StyleAnimation.IsActive="True".

C#

using System;
using System.Collections;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media.Animation;

namespace WpfApplication5
{
  public partial class Window1 : Window
  {
    public Window1()
    {
      InitializeComponent();
    }
  }

  public class StyleAnimation : DependencyObject
  {
    private const int DURATION_MS = 200;

    private static readonly Hashtable _hookedElements = new Hashtable();

    public static readonly DependencyProperty IsActiveProperty =
      DependencyProperty.RegisterAttached("IsActive",
      typeof(bool),
      typeof(StyleAnimation),
      new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsActivePropertyChanged)));

    public static bool GetIsActive(UIElement element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(IsActiveProperty);
    }

    public static void SetIsActive(UIElement element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }
      element.SetValue(IsActiveProperty, value);
    }

    static StyleAnimation()
    {
      // You can specify any owner type, derived from FrameworkElement.
      // For example if you want to animate style for every Control derived
      // class - use Control. If Label is your single target - set it to label.
      // But be aware: value coercion will be called every time your style is
      // updated. So if you have performance problems, probably you should
      // narrow owner type to your exact type.
      FrameworkElement.StyleProperty.AddOwner(typeof(Control),
                                              new FrameworkPropertyMetadata(
                                                null, new PropertyChangedCallback(StyleChanged), CoerceStyle));
    }

    private static object CoerceStyle(DependencyObject d, object baseValue)
    {
      var c = d as Control;
      if (c == null || c.Style == baseValue)
      {
        return baseValue;
      }

      if (CheckAndUpdateAnimationStartedFlag(c))
      {
        return baseValue;
      }
      // If we get here, it means we have to start our animation loop:
      // 1. Hide control with old style.
      // 2. When done set control's style to new one. This will reenter to this
      // function, but will return baseValue, since CheckAndUpdateAnimationStartedFlag()
      // will be true.
      // 3. Show control with new style.

      var showAnimation = new DoubleAnimation
      {
        Duration =
          new Duration(TimeSpan.FromMilliseconds(DURATION_MS)),
        To = 1
      };


      var hideAnimation = new DoubleAnimation
      {
        Duration = new Duration(TimeSpan.FromMilliseconds(DURATION_MS)),
        To = 0
      };

      hideAnimation.Completed += (o, e) =>
      {
        // To stress it one more: this will trigger value coercion again,
        // but CheckAndUpdateAnimationStartedFlag() function will reture true
        // this time, and we will not go to this loop again.
        c.CoerceValue(FrameworkElement.StyleProperty);
        c.BeginAnimation(UIElement.OpacityProperty, showAnimation);
      };

      c.BeginAnimation(UIElement.OpacityProperty, hideAnimation);
      return c.Style; // Return old style this time.
    }

    private static void StyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      // So what? Do nothing.
    }

    private static void OnIsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var fe = d as FrameworkElement;
      if (fe == null)
      {
        return;
      }
      if (GetIsActive(fe))
      {
        HookStyleChanges(fe);
      }
      else
      {
        UnHookStyleChanges(fe);
      }
    }

    private static void UnHookStyleChanges(FrameworkElement fe)
    {
      if (_hookedElements.Contains(fe))
      {
        _hookedElements.Remove(fe);
      }
    }

    private static void HookStyleChanges(FrameworkElement fe)
    {
      _hookedElements.Add(fe, false);
    }

    private static bool CheckAndUpdateAnimationStartedFlag(Control c)
    {
      var hookedElement = _hookedElements.Contains(c);
      if (!hookedElement)
      {
        return true; // don't need to animate unhooked elements.
      }

      var animationStarted = (bool)_hookedElements[c];
      _hookedElements[c] = !animationStarted;

      return animationStarted;
    }
  }

  public class BooleanStyleConverter : IValueConverter
  {
    public Style StyleFalse { get; set; }
    public Style StyleTrue { get; set; }
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
      if ((bool)value)
      {
        return StyleTrue;
      }
      return StyleFalse;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
      throw new NotImplementedException();
    }
  }
}

Cheers :)

Anvaka
Thank you! As both you and Justin pointed out though, I have to reconsider what it is I'm actually trying to do, so I'll go with his answer. But many cookies to you for an elegant solution!
Zissou
A: 

I too got a solution becuase I as well liked your problem :) I solved the puzzle using ColorAnimations on the gradient. Have a look:

<Window x:Class="WpfTest___App.DoEvents.Window1"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="Window1" MinHeight="300" MinWidth="300"
 Name="Window">
<Grid DataContext="{Binding ElementName=Window}">
 <Grid.RowDefinitions>
  <RowDefinition Height="*"/>
  <RowDefinition Height="*"/>
 </Grid.RowDefinitions>
 <Label Content="Some content here" Grid.Row="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
  <Label.Background>
   <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
    <GradientStop Color="Blue" Offset="0"/>
    <GradientStop Color="LightBlue" Offset="0.4"/>
    <GradientStop Color="White" Offset="1"/>
   </LinearGradientBrush>
  </Label.Background>
  <Label.Style>
   <Style>
    <Style.Triggers>
     <DataTrigger Binding="{Binding IsCheckedOut}" Value="True">
      <DataTrigger.EnterActions>
       <BeginStoryboard>
        <Storyboard>
         <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[0].Color" 
             To="Red" Duration="0:0:5"/>
         <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[1].Color" 
             To="Orange" Duration="0:0:5"/>
        </Storyboard>
       </BeginStoryboard>
      </DataTrigger.EnterActions>
      <DataTrigger.ExitActions>
       <BeginStoryboard>
        <Storyboard>
         <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[0].Color" 
             To="Blue" Duration="0:0:5"/>
         <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[1].Color" 
             To="LightBlue" Duration="0:0:5"/>
        </Storyboard>
       </BeginStoryboard>
      </DataTrigger.ExitActions>
     </DataTrigger>
    </Style.Triggers>
   </Style>
  </Label.Style>
 </Label>
 <Button Content="Change IsCheckedOut" Click="Button_Click" Grid.Row="1"/>
</Grid>

In the code behind I created a Dependency property for testing and a listener to the Click event of the button:

public partial class Window1 : Window
{
 public bool IsCheckedOut
 {
  get { return (bool)GetValue(IsCheckedOutProperty); }
  set { SetValue(IsCheckedOutProperty, value); }
 }
 public static readonly DependencyProperty IsCheckedOutProperty = DependencyProperty.Register("IsCheckedOut", typeof(bool), typeof(Window1), new PropertyMetadata(false));

 public Window1()
 {
  InitializeComponent();
 }

 private void Button_Click(object sender, RoutedEventArgs e)
 {
  IsCheckedOut = !IsCheckedOut;
 }
}

This should solve your problem as well :)

Zenuka
This was actually exactly what I needed, or at least what I thought I needed. However, Justin opened my eyes to what it is I'm really trying to do - change states - so I'm going with his suggestion.. Many thanks for the help though!
Zissou