Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Experiments in Building a Silverlight Photo Control ( Part 3 )

Blogs

Mike Taulty's Blog

Elsewhere

Archives

Following on from this post, and with reference to these posts by scorbs and this one by Ian, I thought it was time that my control got some states.

I didn't want to be too ambitious with this so I figured that my control perhaps just needed some basic states which seem to be fairly common across all controls. I put these into two groups ( just like they are for the built-in controls );

  1. CommonStates
    1. Normal
    2. MouseOver
    3. Disabled
  2. FocusStates
    1. Focused
    2. Unfocused

I also came up with a bunch of ImageStates but, after playing around with that, I've not implemented them properly yet.

One of the things that surprised me about adding these states is that I found myself having to teach my control how to know whether it is currently in a mouse-over state and whether it has focus and so on whereas I was kind of expecting these to be common enough to be built into a base class.

So, for instance, in order for my control to do the focus/unfocus thing I ended up;

  1. Adding a dependency property to the control - IsFocused.
  2. Sync'ing up to the base class events GotFocus and LostFocus and changing that property.

I know it's not a lot of work but I found it strange to be creating my own DependencyProperty called IsFocusedProperty and also that other controls ( e.g. ButtonBase, Slider, etc ) do exactly the same thing.

So, I added a bunch of metadata to my control hoping that I'll use that in a future post to style the control in Expression Blend. Here's the whole class because it's grown quite a bit;

  [TemplatePart(Name = PhotoControl.PreviousButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.NextButtonElement, Type = typeof(Button))]
  [TemplatePart(Name = PhotoControl.ImageElement, Type = typeof(Image))]
  [TemplatePart(Name = PhotoControl.TextElement, Type = typeof(TextBlock))]
  [TemplatePart(Name = PhotoControl.LoadingElement, Type = typeof(FrameworkElement))]
  [TemplatePart(Name = PhotoControl.RootElement, Type = typeof(Panel))]
  [TemplateVisualState(GroupName = "CommonStates", Name="Normal")]
  [TemplateVisualState(GroupName = "CommonStates", Name = "MouseOver")]
  [TemplateVisualState(GroupName = "CommonStates", Name = "Disabled")]
  [TemplateVisualState(GroupName = "FocusStates", Name = "Focused")]
  [TemplateVisualState(GroupName = "FocusStates", Name = "Unfocused")]
  public class PhotoControl : Control 
  {
    public DependencyProperty IsFocusedProperty = DependencyProperty.Register(
      "IsFocused", typeof(bool), typeof(PhotoControl), null);

    public DependencyProperty IsMouseOverProperty = DependencyProperty.Register(
      "IsMouseOver", typeof(bool), typeof(PhotoControl), null);

    public DependencyProperty IsEnabledProperty = DependencyProperty.Register(
      "IsEnabled", typeof(bool), typeof(PhotoControl), null);

    public PhotoControl()
    {
      DefaultStyleKey = typeof(PhotoControl);
      photos = new List<PhotoSource>();
      this.IsEnabled = true;
      this.Loaded += OnLoaded;
      this.GotFocus += OnGotFocus;
      this.LostFocus += OnLostFocus;
      this.MouseEnter += OnMouseEnter;
      this.MouseLeave += OnMouseLeave;
    }
    public bool IsFocused
    {
      get
      {
        return ((bool)base.GetValue(IsFocusedProperty));
      }
      set
      {
        base.SetValue(IsFocusedProperty, value);
      }
    }
    public bool IsEnabled
    {
      get
      {
        return ((bool)base.GetValue(IsEnabledProperty));
      }
      set
      {
        base.SetValue(IsEnabledProperty, value);

        base.IsTabStop = value;

        UpdateVisualState(true);
      }
    }
    public bool IsMouseOver
    {
      get
      {
        return ((bool)base.GetValue(IsMouseOverProperty));
      }
      set
      {
        base.SetValue(IsMouseOverProperty, value);
      }
    }
    void OnGotFocus(object sender, RoutedEventArgs args)
    {
      this.IsFocused = true;
      UpdateVisualState(true);
    }
    void OnLostFocus(object sender, RoutedEventArgs args)
    {
      this.IsFocused = false;
      UpdateVisualState(true);
    }
    void OnMouseEnter(object sender, MouseEventArgs args)
    {
      this.IsMouseOver = true;
      UpdateVisualState(true);
    }
    void OnMouseLeave(object sender, MouseEventArgs args)
    {
      this.IsMouseOver = false;
      UpdateVisualState(true);
    }
    void UpdateVisualState(bool useTransitions)
    {
      // CommonStates group.
      if (!this.IsEnabled)
      {
        VisualStateManager.GoToState(this, "Disabled", useTransitions);
      }
      else
      {
        if (this.IsMouseOver)
        {
          VisualStateManager.GoToState(this, "MouseOver", useTransitions);
        }
        else
        {
          VisualStateManager.GoToState(this, "Normal", useTransitions);
        }
      }
      // Focused states group
      if (this.IsFocused)
      {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
      }
      else
      {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
      }
    }
    void OnLoaded(object sender, RoutedEventArgs e)
    {
      imageLoader = new ImageLoader(photos);
      imageLoader.ImageStreamAvailable += OnImageAvailable;
      imageLoader.Initialise();
    }
    void OnImageAvailable(object sender, StreamEventArgs e)
    {      
      Dispatcher.BeginInvoke(() =>
        {
          BitmapImage bi = new BitmapImage();
          bi.SetSource(e.Stream);
          imageElement.Source = bi;
        });
    }
    public List<PhotoSource> Photos 
    {
      get
      {
        return (photos);
      }
      set
      {
        photos = value;
      }
    }    
    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      prevButton = GetTemplateChild(PreviousButtonElement) as Button;
      nextButton = GetTemplateChild(NextButtonElement) as Button;
      rootElement = GetTemplateChild(RootElement) as FrameworkElement;
      textElement = GetTemplateChild(TextElement) as TextBlock;
      imageElement = GetTemplateChild(ImageElement) as Image;
      loadingElement = GetTemplateChild(LoadingElement) as FrameworkElement;

      SyncButtonHandlers();

      this.DataContext = imageLoader;
    }
    private void SyncButtonHandlers()
    {
      if (prevButton != null)
      {
        prevButton.Click += OnPrevClick;
      }
      if (nextButton != null)
      {
        nextButton.Click += OnNextClick;
      }
    }
    void OnPrevClick(object sender, RoutedEventArgs args)
    {
      if (imageLoader.HasPreviousImage)
      {
        imageLoader.MoveToPreviousImage();
      }
    }
    void OnNextClick(object sender, RoutedEventArgs args)
    {
      if (imageLoader.HasNextImage)
      {
        imageLoader.MoveToNextImage();
      }
    }
    Button prevButton;
    Button nextButton;
    FrameworkElement rootElement;
    TextBlock textElement;
    Image imageElement;
    FrameworkElement loadingElement;
    ImageLoader imageLoader;
    List<PhotoSource> photos;

    public const string RootElement = "RootElement";
    public const string PreviousButtonElement = "PreviousButtonElement";
    public const string NextButtonElement = "NextButtonElement";
    public const string TextElement = "TextElement";
    public const string ImageElement = "ImageElement";
    public const string LoadingElement = "LoadingElement";
  }

What are the main additions there?

  1. Additional metadata to describe my control state groups and the states within them.
  2. Additional properties to reflect IsFocused, IsMouseOver, IsEnabled.
  3. An additional function UpdateVisualState which tries to use the VisualStateManager in order to move to one of the states that the control understands and various calls to it when the state changes.

With that in place, I need to alter the default XAML for my control in order that it has some notion of what the VisualStateManager should do when we move into a state such as MouseOver, Focused and so on.

I've just pasted the XAML for my actual control's style here;

  <Style
    TargetType="local:PhotoControl">
    <Setter
      Property="Template">
      <Setter.Value>
        <ControlTemplate
          TargetType="local:PhotoControl">
          <Grid
            x:Name="RootElement"
            Background="Black">
            <vsm:VisualStateManager.VisualStateGroups>
              <vsm:VisualStateGroup
                x:Name="FocusStates">
                <vsm:VisualState
                  x:Name="Focused">
                  <Storyboard>
                    <DoubleAnimation
                      To="1.0"
                      Storyboard.TargetName="focusElement"
                      Storyboard.TargetProperty="Opacity"
                      Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
                <vsm:VisualState
                  x:Name="Unfocused">
                  <Storyboard>
                    <DoubleAnimation
                      To="0.0"
                      Storyboard.TargetName="focusElement"
                      Storyboard.TargetProperty="Opacity"
                      Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
              </vsm:VisualStateGroup>
              <vsm:VisualStateGroup
                x:Name="CommonStates">
                <vsm:VisualState
                  x:Name="MouseOver">
                  <Storyboard>
                    <ColorAnimation
                      To="White"
                      Storyboard.TargetName="outerBorderBrush"
                      Storyboard.TargetProperty="Color"
                      Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
                <vsm:VisualState
                  x:Name="Normal">                  
                </vsm:VisualState>
                <vsm:VisualState
                  x:Name="Disabled">
                  <Storyboard>
                    <DoubleAnimation
                      To="0.5"
                      Storyboard.TargetName="outerBorder"
                      Storyboard.TargetProperty="Opacity"
                      Duration="0" />
                  </Storyboard>
                </vsm:VisualState>
              </vsm:VisualStateGroup>
              </vsm:VisualStateManager.VisualStateGroups>
            <Rectangle
              Margin="5"
              Stroke="White"
              StrokeThickness="2"
              StrokeDashArray="1,3"
              RadiusX="5"
              RadiusY="5"
              x:Name="focusElement"
              Opacity="0"/>
            <Border
              Margin="10"
              CornerRadius="2"
              BorderThickness="2"
              x:Name="outerBorder">
              <Border.BorderBrush>
                <SolidColorBrush x:Name="outerBorderBrush"
                  Color="Gray" />
              </Border.BorderBrush>
              <Grid>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition
                    Width="*" />
                  <ColumnDefinition
                    Width="8*" />
                  <ColumnDefinition
                    Width="*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                  <RowDefinition
                    Height="*" />
                  <RowDefinition
                    Height="Auto" />
                </Grid.RowDefinitions>
                <Rectangle
                  x:Name="LoadingElement"
                  Grid.ColumnSpan="3"
                  Opacity="0"
                  Fill="Gray" />
                <Button
                  Grid.Column="0"
                  x:Name="PreviousButtonElement"
                  VerticalAlignment="Center"
                  Margin="5,0,5,0"
                  IsEnabled="{Binding HasPreviousImage}"
                  Template="{StaticResource PrevButtonTemplate}" />
                <Border
                  Margin="3"
                  BorderBrush="Silver"
                  CornerRadius="2"
                  BorderThickness="2"
                  Grid.Column="1">
                  <Image
                    x:Name="ImageElement"
                    Margin="5" />
                </Border>
                <Button
                  Grid.Column="2"
                  x:Name="NextButtonElement"
                  VerticalAlignment="Center"
                  Margin="5,0,5,0"         
                  IsEnabled="{Binding HasNextImage}"
                  Template="{StaticResource NextButtonTemplate}" />
                <TextBlock
                  Grid.Row="1"
                  Grid.Column="1"
                  Foreground="White"
                  x:Name="TextElement"
                  Text="{Binding TextLabel}"
                  TextAlignment="Center"
                  VerticalAlignment="Center" />
              </Grid>
            </Border>
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

The changes there are that we have ( within the root element of my control ) some definitions for VisualStateGroups and, specifically, there's some slight alterations to the state of various items such as;

  1. MouseOver - I change the colour of my external border from "grey" to white.
  2. Focused - I make a little dashed rectangle around the edge of my control visible.
  3. Disabled - I change the opacity of the border that frames my control.

I altered the hosting XAML page in order to have a button that would enable/disable my control and then rehosted it;

image

and it all seems to work "reasonably" although I'm not sure that what I'm doing with the VSM is quite right.

I do have one minor glitch though that I've yet to figure out. My "Next" and "Previous" buttons have enabled/disabled states. When I first run up the application, we're displaying the 1st image so the "Next" button should be enabled and the "Previous" button should be disabled.

That's the case.

However, when I mouse into the control or give it focus I find that my "Next" button magically enters its disabled state - I've yet to figure out quite why that is.

Next, I'll want to take my project over to Expression Blend and see if I can style my control using that.


Posted Sat, Jul 19 2008 3:59 AM by mtaulty
Filed under: ,

Comments

Dew Drop - July 19, 2008 | Alvin Ashcraft's Morning Dew wrote Dew Drop - July 19, 2008 | Alvin Ashcraft's Morning Dew
on Sat, Jul 19 2008 6:36 AM
Mike Taulty's Blog wrote Experiments in Building a Silverlight Photo Control ( Part 4 )
on Mon, Jul 21 2008 3:38 AM
Following on from this post, I wanted to see what would happen if I opened my solution in Expression...
Christopher Steen wrote Link Listing - July 20, 2008
on Mon, Jul 21 2008 5:42 AM
WPF  Finding a XAML Element by Name at Runtime [Via: andy@beaulieu.com ]  Dynamic XAML [Via: chrishayuk...
Community Blogs wrote Silverlight Cream for July 21, 2008 - 2 -- #331
on Mon, Jul 21 2008 11:06 PM
Martin Mihaylov on the MultiscaleImage control, Alan Cobb on changing Silverlight properties dynamically
Marc: My Words wrote Interesting stuff from the past few days
on Tue, Jul 22 2008 5:52 AM
These days, it’s not so much email as RSS that causes the delays when I get back from leave. Here’s a
Marc: My Words wrote Interesting stuff from the past few days
on Tue, Jul 22 2008 5:52 AM
Interesting stuff from the past few days