Sunday 16 January 2011

Lookless Carousel Control in Silverlight

One of the tasks I had when designing the kiosk application for DeepVisuals, was the need for a background which changes every so often.

Ok, so this would be very easy to brute force; a couple of storyboard and hard-coded images and we're done. But I wanted to take this as an opportunity to develop my understanding about parts and states and look-less controls. Basically our end idea for the kiosk application is to make it skin-able so a look-less control which is styled separately is the best way forward.

So I started off by reading a few blogs and post, got stuck in. This was what I came up with:

[TemplatePart(Name = "PART_CarouselGrid", Type = typeof(Grid))]
    public class Carousel : Control
    {
        protected Queue<UIElement> m_elements = new Queue<UIElement>();
        protected DispatcherTimer m_cycleCarouselTimer = new DispatcherTimer();

        protected Storyboard m_fadeStoryboard;

        protected DoubleAnimation m_fadeOutAnimation;
        protected DoubleAnimation m_fadeInAnimation;

        protected Grid m_carouselGrid;

        public Carousel()
        {
            DefaultStyleKey = typeof(Carousel);
        }

        /// 
        /// Dependency property for the duration of a transiton.
        /// 
        public static DependencyProperty TransitionDurationProperty = DependencyProperty.Register(
            "TransitionDuration",
            typeof(double),
            typeof(Carousel),
            new PropertyMetadata(5d));

        /// 
        /// Dependency property for the duration each carousel item is displayed for.
        /// 
        public static DependencyProperty DisplayDurationProperty = DependencyProperty.Register(
            "DisplayDuration",
            typeof(double),
            typeof(Carousel),
            new PropertyMetadata(30d));

        /// 
        /// The duration of the transition between items in the carousel.
        /// 
        public double TransitionDuration
        {
            get { return (double)GetValue(TransitionDurationProperty); }
            set { SetValue(TransitionDurationProperty, value); }
        }

        /// 
        /// The duration each carousel item is displayed for.
        /// 
        public double DisplayDuration
        {
            get { return (double)GetValue(DisplayDurationProperty); }
            set { SetValue(DisplayDurationProperty, value); }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // Get the grid used for storing backgrounds
            m_carouselGrid = (Grid)GetTemplateChild("PART_CarouselGrid");

            LoadCarousel();
        }

        private void LoadCarousel()
        {
            // Get the images in the background holder
            foreach (UIElement element in m_carouselGrid.Children)
            {
                element.Opacity = 0;
                m_elements.Enqueue(element);
            }

            if (m_elements.Count == 0)
            {
                // If there are no images, we can't really do much
                throw new ArgumentException("Carousel must contain at least 1 UIElement");
            }
            else if (m_elements.Count > 1)
            {
                // Set up the storyboards 
                SetupAnimations();

                // If we have enough images to fade between, then lets start the timer
                m_cycleCarouselTimer.Interval = TimeSpan.FromSeconds(DisplayDuration);
                m_cycleCarouselTimer.Tick += new EventHandler(CycleCarouselTimer_Tick);
                m_cycleCarouselTimer.Start();
            }

            // Set the first image to be visible for starters
            m_elements.Peek().Opacity = 1;
        }

        /// 
        /// Fades out the current image in the carousel
        /// 
        public virtual void Start()
        {
            m_cycleCarouselTimer.Start();
        }

        /// 
        /// Fades in the current image in the carousel
        /// 
        public virtual void Stop()
        {
            m_cycleCarouselTimer.Stop();
        }

        private void SetupAnimations()
        {
            m_fadeStoryboard = new Storyboard();

            m_fadeInAnimation = new DoubleAnimation();
            m_fadeInAnimation.To = 1d;
            m_fadeInAnimation.Duration = TimeSpan.FromSeconds(TransitionDuration);
            Storyboard.SetTargetProperty(m_fadeInAnimation, new PropertyPath("(UIElement.Opacity)"));

            m_fadeOutAnimation = new DoubleAnimation();
            m_fadeOutAnimation.To = 0d;
            m_fadeOutAnimation.Duration = TimeSpan.FromSeconds(TransitionDuration);
            Storyboard.SetTargetProperty(m_fadeOutAnimation, new PropertyPath("(UIElement.Opacity)"));

            m_fadeStoryboard.Children.Add(m_fadeInAnimation);
            m_fadeStoryboard.Children.Add(m_fadeOutAnimation);

            m_fadeStoryboard.Completed += new EventHandler(ElementFade_Completed);
        }

        private void ElementFade_Completed(object sender, EventArgs e)
        {
            // Set the final values before stopping the storyboard so we can re-use.
            m_elements.First().Opacity = 1d;
            m_elements.Last().Opacity = 0d;
            m_fadeStoryboard.Stop();

            // Call the virtual method to alert anyone overriding
            OnCycleCompleted();
        }

        private void CycleCarouselTimer_Tick(object sender, EventArgs e)
        {
            OnCycleCarousel();
        }

        protected virtual void OnCycleCompleted()
        {

        }

        protected virtual void OnCycleCarousel()
        {
            UIElement fadeOutImage = m_elements.Dequeue();
            UIElement fadeInImage = m_elements.Peek();

            Storyboard.SetTarget(m_fadeOutAnimation, fadeOutImage);
            Storyboard.SetTarget(m_fadeInAnimation, fadeInImage);

            m_fadeStoryboard.Begin();
            m_elements.Enqueue(fadeOutImage);
        }
    }

So the beauty of this control is that the items (or images) that it scrolls through periodically are actually defined in the style for the control. That's what the line below is for:

[TemplatePart(Name = "PART_CarouselGrid", Type = typeof(Grid))]

Basically it says that when you style this control, it must have a control in, which is a Grid and it must be called 'PART_CarouselGrid'. This makes it possible for the look-less control to get a reference to the Grid in OnApplyTemplate. From here we can then go through all the elements in the grid (specified in the style) and use them as the elements we'll rotate through.

A sample style you could use with the background would be similar to below, which basically says each item will be shown for 30 seconds, the transition between items will take 1 second and the items to switch between will simply be a black background and a white background (not interesting I know, but hey, its an example!). :)



And in the application, the background is added using:

<my:Carousel Style="blah" />

Simples!

No comments:

Post a Comment