vrijdag 10 juli 2009

A viewmodel for the fluidkit transition control

I was trying to create a program that has several pages with only one page visible at a time. If you want to see another page you should press a button with the name of that page.
The first thing I tried was using a TabControl. The problem with the TabControl was that each time you left a page the page would be unloaded. This is not the behaviour I wanted. The pages should stay in memory even when they aren’t visible. I searched for a solution and found this blog entry that described an extension to the tabcontrol called TabControlEx. I tried it out and indeed the items in the tab stayed in memory, but I couldn’t get it to work the way I wanted it.
Then I remembered the fluidkit library created by Pavan Podila. This library has a class called TransitionPresenter that derives from ItemsControl. You can make a slideshow with the items you add to this control. The items you add stay in memory when they aren’t shown on screen. Because I always try to write my programs with the MVVM pattern I looked for a viewmodel for this class. I found a blog entry by Jeremy Alles who extended the TransitionPresenter and created a viewmodel for his newly created class. The strange thing was that his new class loaded the visible pages and unloaded the invisible pages. I tried to change this behaviour in his class but couldn’t figure out what happened and gave up.

After my failed attempts with the TabControl and TabControlEx I decided to create my own viewmodel for the TransitionPresenter class. It turned out that the solution was relative easy. First I had to extend the TransitionPresenter class. I used attached properties to extend the behaviour of the TransitionPresenter class. I basically added one new attached dependency property. When you set the property called WorkSpaceNumber there will be a transition to the slide with this index number. If the index of the new slide is higher than the old one the transition animation will make a forward movement if the index is lower the transition will go backward. The static behaviour class you can see below:

public static class TransitionPresenterBehaviour
    {
        public static int GetWorkspaceNumber(DependencyObject obj)
        {
            return (int)obj.GetValue(WorkspaceNumberProperty);
        }
 
        public static void SetWorkspaceNumber(DependencyObject obj, int value)
        {
            obj.SetValue(WorkspaceNumberProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for WorkspaceNumber.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty WorkspaceNumberProperty =
            DependencyProperty.RegisterAttached("WorkspaceNumber", typeof(int),
            typeof(TransitionPresenterBehaviour),
            new UIPropertyMetadata(-1, OnIndexChanged));
 
        private static void OnIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TransitionPresenter tp = (TransitionPresenter)d;
            int newIndex = (int)e.NewValue;
            int oldIndex = (int)e.OldValue;
            FrameworkElement elNew = (FrameworkElement)tp.ItemContainerGenerator.ContainerFromItem(tp.Items[newIndex]);
            FrameworkElement elOld = (oldIndex > -1) ?
                (FrameworkElement)tp.ItemContainerGenerator.ContainerFromItem(tp.Items[oldIndex]) : elNew;
            if (elNew != null)
            {
                TransitionPresenterBehaviour.SetForWard(tp,newIndex > oldIndex);
                tp.ApplyTransition(elOld, elNew);
            }
        }
 
        public static bool GetForWard(DependencyObject obj)
        {
            return (bool)obj.GetValue(ForWardProperty);
        }
 
        public static void SetForWard(DependencyObject obj, bool value)
        {
            obj.SetValue(ForWardProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for ForWard.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ForWardProperty =
            DependencyProperty.RegisterAttached("ForWard", typeof(bool), 
            typeof(TransitionPresenterBehaviour),
            new UIPropertyMetadata(false, ForwardChanged));
 
        private static void ForwardChanged(DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            TransitionPresenter tp = (TransitionPresenter)d;
            forwardChanged(tp,(bool)e.OldValue, (bool)e.NewValue);
        }
 
        private static void forwardChanged(TransitionPresenter tp, bool oldRot, bool newRot)
        {
            if (oldRot == newRot)
                return ;
            if (tp.Transition is SlideTransition)
                (tp.Transition as SlideTransition).Direction = (newRot) ? Direction.LeftToRight : Direction.RightToLeft;
            if (tp.Transition is CubeTransition)
                (tp.Transition as CubeTransition).Rotation = (newRot) ? Direction.LeftToRight : Direction.RightToLeft;
            if (tp.Transition is FlipTransition)
                (tp.Transition as FlipTransition).Rotation = (newRot) ? Direction.LeftToRight : Direction.RightToLeft;
            if (tp.Transition is GenieTransition)
                (tp.Transition as GenieTransition).EffectType = (newRot) ? GenieEffectType.IntoLamp : GenieEffectType.OutOfLamp;
        }
    }
In the OnIndexChanged method the actual transition happens. Before the transition the program checks if the new index is higher than the old one. If this is the case a forward transition is shown otherwise a backward one.
The viewmodel for the TransitionPresenterEx class I called FluidViewModel. It is derived it from MainWindowViewModel, a class that Josh Smith describes in this article. I made a few changes to this class and it’s underlying classes. I described some of these changes in my previous blogs here and here. The source code for the FluidViewModel looks like this:
    /// <summary>
    /// A viewmodel for the fluidkit transitionpresenter class.
    /// </summary>
    public class FluidViewModel : MainWindowViewModel
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="FluidViewModel"/> class.
        /// </summary>
        public FluidViewModel(): base()
        {
            this._collectionView.CurrentChanged += new EventHandler(_collectionView_CurrentChanged);
            this._collectionView.Refresh();
        }
 
        /// <summary>
        /// Handles the CurrentChanged event of the _collectionView control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        void _collectionView_CurrentChanged(object sender, EventArgs e)
        {
            int to = this.Workspaces.IndexOf(this.ActiveWorkSpace);
            // to update the Direction property
            WorkspaceNumber=to;
        }
 
        private bool forward;
 
        /// <summary>
        /// Gets or sets the direction in which the pages slide.
        /// </summary>
        /// <value>The direction.</value>
        public bool Forward
        {
            get
            {
                return this.forward;
            }
            set
            {
                this.forward = value;
                RaisePropertyChanged(() => Forward);
            }
        }
 
        private int workspaceNumber=-1, oldWorkspaceNr;
 
        /// <summary>
        /// Gets or sets the workspace number.
        /// </summary>
        /// <value>The workspace number.</value>
        public int WorkspaceNumber
        {
            get
            {
                return this.workspaceNumber;
            }
            set
            {
                oldWorkspaceNr = this.workspaceNumber;
                this.workspaceNumber = value;
                // check the orientation of the change made by the user
                this.Forward = (this.oldWorkspaceNr < this.workspaceNumber);
                if (workspaceNumber != oldWorkspaceNr)
                    RaisePropertyChanged(() => WorkspaceNumber);
            }
        }
 
        /// <summary>
        /// Creates the commands.
        /// </summary>
        /// <returns>returns a list of commands</returns>
        protected override List<CommandViewModel> CreateCommands()
        {
            List<CommandViewModel> commands = new List<CommandViewModel>();
            foreach (WorkspaceViewModel ws in Workspaces)
                commands.Add(new CommandViewModel(ws.DisplayName, ws.ShowCommand));
            return commands;
        }
    }

There is a property called WorkSpaceNumber with which you can get and set the currently selected slide. The Forward property is set to true if the old workspace number is smaller than the new workspace number.  The CreateCommands method is an abstract method of the MainWindowViewModel and create a list of commands in the FluidViewModel that are used to show the workspace if executed.
As an example I created a small program with two images and two buttons. First a viewmodel for the main window of the program must be created. This is now very simple:
    public class ImagesViewModel:FluidViewModel
    {
        Image1ViewModel image1;
 
        public Image1ViewModel Image1
        {
            get { return image1; }
            private set
            {
                image1 = value;
                RaisePropertyChanged(() => Image1);
            }
        }
 
        Image2ViewModel image2;
 
        public Image2ViewModel Image2
        {
            get { return image2; }
            private set
            {
                image2 = value;
                RaisePropertyChanged(() => Image2);
            }
        }
 
        public ImagesViewModel()
        {
            image1 = new Image1ViewModel(this);
            image2 = new Image2ViewModel(this);
            Workspaces.Add(image1);
            Workspaces.Add(image2);
        }
    }
Image1ViewModel and Image2ViewModel are both derived from the WorkspacesViewModel and are added to the list of workspaces of the MainWindowViewModel in the constructor.
Connecting the ImagesViewModel to the TransitionPresenterEx control is now very easy. An example you can see in the xaml code below:
       <DockPanel>
            <ListBox DockPanel.Dock="Top" ItemsSource="{Binding Path=Commands}" 
                     ItemTemplate="{StaticResource CommandsTemplate}">
            </ListBox>
            <ContentControl Name="workspacesContent" 
                            Content="{Binding}"
                            ContentTemplate="{StaticResource WorkspacesTemplate}"/>
        </DockPanel>
On top is a listbox. The ItemsSource property of the listbox binds to the Commands property of the FluidViewModel. A DataTemplate called CommandsTemplate changes the commands into buttons that hold the ShowCommand commands.
The datatemplate called WorkspacesTemplate changes the ImagesViewModel in a TransionPresenterEx object.
The most important datatemplates and styles that are used you can see below:
    <DataTemplate DataType="{x:Type vm:Image1ViewModel}">
        <vw:Image1Control/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:Image2ViewModel}">
        <vw:Image2Control/>
    </DataTemplate>
    <DataTemplate x:Key="CommandsTemplate">
        <Button Command="{Binding Path=Command}" Content="{Binding Path=DisplayName}">
        </Button>
    </DataTemplate>
    <fl:SlideTransition x:Key="SlideTransition" />
    <DataTemplate x:Key="WorkspacesTemplate" >
        <fl:TransitionPresenter
            RestDuration="0:0:3"
            Transition="{StaticResource SlideTransition}"
            ItemsSource="{Binding Path=Workspaces}"
            vw:TransitionPresenterBehaviour.WorkspaceNumber="{Binding Path=WorkspaceNumber}">
        </fl:TransitionPresenter>
    </DataTemplate>
The source code you can download here. You also have to download the fluidkit library here and make a reference to it in the demonstration program. Below you can see an image of the program.
image
Shout it kick it on DotNetKicks.com

4 opmerkingen:

Jeremy Alles zei

Robert,

This looks very cool ! I love the attached behavior and I think this is a great idea to use it here.

However I cannot build your sample, it seems that one of the project is missing. You could please check it out ?

Robert zei

Hi,

Thanks for the compliments.

I made a mistake and added an old library. The correct library is now included and everything should work now.

Robert

Anoniem zei

Robert,

I newly started learning WPF, MVVM and Entity Framework. Previously I read Beth Massi’s, Josh Smith’s, Vincent Sibal's and Karl Shifflett’s blogs. Finally I found your blog. Jason Dolinger’s video was great and I would like to thank you for sharing it.

In your recent fluidkit sample you put all the good things in your Wpf.MVVM project and I found them very usefull. Can you post simple examples that shows the usage of new ViewModels that you added to your library especially DialogViewModel?

Keep up with the good work.

Robert zei

Thanks,

I wrote the DialogViewModel a long time ago but I haven't had time yet to publish an example. I will try to write a blog post with examples as soon as possible now. So maybe next week you can expect a new article about my dialogviewmodel.