Pixel-in-Gene

Exploring Creativity with Design / Graphics / Technology

Using Attached Properties for View Transitions, the ViewManager

Attached Properties is a wonderful feature of WPF and I find myself using it in a variety of scenarios. The most recent one has to do with view-management and transitions. The app that I was building has many visual parts that can be swapped out and replaced with something else. Each part is actually a UserControl, also called the view. To make things interesting, the actual swap happens with a transition, using the TransitionPresenter from the FluidKit library.

Since there are many such areas in the UI, which have changeable views, there are many TransitionPresenters that act as the containers for the views. Swapping of the views typically happens via user interaction and there is also an action that needs to get invoked whenever a view changes. In the first iteration of my solution, I had some code-behind which would do the view-swap and call an action when the transition completed. It looked something like the snippet below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void ExecuteFlip(object parameter)
{
    string prevFace = (string) parameter;
    string nextFace = string.Empty;

    if (prevFace == "Preferences")
    {
        _flipTransition.Rotation = Direction.RightToLeft;
        nextFace = "Main";
    }
    else if (prevFace == "Main")
    {
        _flipTransition.Rotation = Direction.LeftToRight;
        nextFace = "Preferences";
    }

    EventHandler handler = null;
    handler = delegate
                  {
                      _transContainer.TransitionCompleted -= handler;
                      /* Do some action */
                  };
    _transContainer.TransitionCompleted += handler;
    _transContainer.ApplyTransition(prevFace, nextFace);
}
  

ExecuteFlip is called on some user-interaction, generally via a command invoked on a button-click. Depending on the parameter (which is the current view-name) I decide what should be the next view and also adjust the transition to use. Before invoking the transition, I attach an event-handler for the TransitionPresenter’s TransitionCompleted event. This is needed to invoke some actions after the transition completes.

As you can see, for a UI that has many such TransitionPresenters, the code-behind can get pretty repetitive and messy. It’s too much duplication and can easily overwhelm anyone maintaining this code. Fortunately, attached properties come to the rescue.

The need for a ViewManager

To simplify this process of invoking a transition, attaching to the TransitionCompleted event and doing some post-transition action, I have come up with the static ViewManager class that exposes a single attached property called ViewName (of type string). Within the property changed handler for this atttached property, I discover the parent TransitionPresenter and cache in a Dictionary. The Dictionary is just a map of view-name to TransitionPresenter. Here is the code that shows all of these pieces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static Dictionary<string, TransitionPresenter> _mapper;

public static readonly DependencyProperty ViewNameProperty = DependencyProperty.RegisterAttached(
    "ViewName", typeof (string), typeof (ViewManager), new PropertyMetadata(OnViewNameChanged));

static ViewManager()
{
    _mapper = new Dictionary<string, TransitionPresenter>();
}

public static void SetViewName(DependencyObject d, string name)
{
    d.SetValue(ViewNameProperty, name);
}

private static void OnViewNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    UserControl view = d as UserControl;
    if (view == null) throw new ArgumentException("The ViewManager.ViewName attached can only be used on a UserControl");

    string viewName = (string) e.NewValue;
    if (string.IsNullOrEmpty(viewName)) throw new ArgumentException("ViewName cannot be null or empty");

    view.Loaded += FindTransitionPresenter;
}

private static void FindTransitionPresenter(object sender, RoutedEventArgs e)
{
    UserControl view = (UserControl) sender;
    view.Loaded -= FindTransitionPresenter;

    DependencyObject parent = VisualTreeHelper.GetParent(view);
    while (parent != null && !(parent is TransitionPresenter))
    {
        parent = VisualTreeHelper.GetParent(parent);
    }

    if (parent == null)
    {
        throw new Exception("Could not find a TransitionPresenter in the parent chain of view");
    }
    TransitionPresenter presenter = (TransitionPresenter) parent;
    string viewName = (string)view.GetValue(ViewNameProperty);
    _mapper.Add(viewName, presenter);
}
  

The interesting part here is the fact that I hook into the view’s (UserControl) Loaded event to discover the TransitionPresenter. The Loaded event is fired when WPF has created the visual tree and run a first pass of layout and rendering. Hooking into this event gives us a safe way to navigate the visual tree. The actual discovery of the TransitionPresenter happens in the FindTransitionPresenter method. This method also adds the link between the view-name and the TransitionPresenter and stores it in the map. At the end of this, we have a way to know the TransitionPresenter, given the string view-name.

Time for transition

Before we can invoke the transition, we need to associate the ViewName attached property to all of our views. Additionally these views need to be nested inside a TransitionPresenter, since we check for that in the FindTransitionPresenter method. Here is a snippet of adding this attached property:

1
2
3
4
5
6
7
8
9
<Controls:TransitionPresenter Transition="{StaticResource FlipTransition}"
                              Focusable="False"
                              KeyboardNavigation.IsTabStop="False">
    <Views:PreferencesView x:Name="Preferences"
                           Controls1:ViewManager.ViewName="Preferences" />
    <Views:MainView x:Name="Main"
                    Controls1:ViewManager.ViewName="Main" />
</Controls:TransitionPresenter>
  

With that done, we are now in a position to actually invoke the transitions and to do that we have the ViewManager.SwitchView method. Since for a transition you need two views, SwitchView takes the view-names as its parameters. We actually have three different overloads of this method:

1
2
3
4
5
6
public static void SwitchView(string fromView, string toView)

public static void SwitchView(string fromView, string toView, Action onViewSwitchCompleted)

public static void SwitchView(string fromView, string toView, Action onViewSwitchCompleted, Action<TransitionPresenter> configure)
  

The first overload should be obvious, it does a simple transition between the views and nothing more. The second overload gives you a callback method that will be invoked once the transition is complete. The last overload gives you the additional capability to configure your TransitionPresenter before the transition happens. I have used this overload to change the transition for some views. Now lets look at the SwitchView method. I am just showing the third overload, as the other two just call into it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void SwitchView(string fromView, string toView, Action onViewSwitchCompleted, Action<TransitionPresenter> configure)
{
    TransitionPresenter fromTP = _mapper[fromView];
    TransitionPresenter toTP = _mapper[toView];

    if (fromTP != toTP) throw new Exception("TransitionPresenters of From and To views don't match");

    EventHandler handler = null;
    handler = delegate
    {
        fromTP.TransitionCompleted -= handler;

        if (onViewSwitchCompleted != null)
        {
            onViewSwitchCompleted();
        }
    };
    fromTP.TransitionCompleted += handler;

    if (configure != null)
    {
        configure(fromTP);
    }
    fromTP.ApplyTransition(fromView, toView);
}
  

The code here checks if the two views share the same TransitionPresenter and throws an exception otherwise. It then hooks into the TransitionCompleted event and fires the callback when the transition completes. It also calls the configure callback to do any pre-transition configuration on the TransitionPresenter. You can see how the boilerplate code from the first implementation is nicely wrapped up over here.

Using the ViewManager

If you have already setup the ViewName attached properties, using the ViewManager is as simple as calling SwitchView with the correct view-names. If you have some action to be carried out at the end of the transition, you can also pass in an Action delegate. I use the Action delegate to set focus on some controls or set some UI properties. The ExecuteFlip method that I mentioned earlier now looks like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void ExecuteFlip(object parameter)
{
    string prevFace = (string) parameter;
    string nextFace = prevFace == "Preferences" ? "Main" : "Preferences";

    Action onSwitch = () => { /* Do some action */ };
    Action<TransitionPresenter> configure = (tp) =>
                                                {
                                                    FlipTransition t = (FlipTransition)tp.Transition;
                                                    t.Rotation = prevFace == "Preferences" ? Direction.RightToLeft : Direction.LeftToRight;
                                                };
    ViewManager.SwitchView(prevFace, nextFace, onSwitch, configure);
}
  

Some Takeaways

  • One can argue that using strings for view-names is not type-safe and should instead be enums. But then you limit yourself to a particular enum-type and need to constantly update it as new views are added. One way to have type safety would be to expose static string constants on your App and use them instead.
  • You can also have some setup methods on the ViewManager to do any global configuration. You could call this once your main window is loaded. I use it to setup some default durations on all TransitionPresenters.
  • One big advantage of the ViewManager is that a transition on any part of the UI can be invoked from any other part of the UI. All I need to know are the view-names that are involved in the transition and just call ViewManager.SwitchView. Internally the correct TransitionPresenter is determined and used for the transition. This has been a great feature for me as there are some non-user-events that cause transitions on some views.

Comments