Friday, October 21, 2011

Using Jounce Navigation to Create OOB Child Windows in Silverlight 5

One of the reasons I prefer to manage navigation as an event, rather than a strongly typed interface or handler, is because it allows for so much flexibility and extensibility in the navigation pipeline. In my Jounce framework, for example, the basic navigation event simply wires up an instance of a view to a view model and makes no presumptions about where that view belongs - it leaves positioning the view to the developer. The region manager simply listens for navigation events and passes the views off to region adapters without worrying about how they are wired, so it automatically handles the view simply by extending the pipeline by listening for the message as well. This same model makes it incredibly simple to place views in child windows using the new Silverlight 5 Out-of-Browser feature.

The first thing I'll do is create a controller to listen to navigation messages. It will expect a specific parameter to be passed that indicates when the view should be passed to a window. If that parameter exists, it will use parameters for height, width, and title to spin up the new window. A more complete implementation would store those literals as constants. Here is the shell for the class that implements the listener and subscribes to it:

[Export]
public class WindowController : IEventSink<ViewNavigationArgs>,
                                IPartImportsSatisfiedNotification
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    [Import]
    public IViewModelRouter Router { get; set; }
    
    public void OnImportsSatisfied()
    {
        EventAggregator.SubscribeOnDispatcher(this);
    }

Now the handler. The handler will check the parameters for a special "OpenInWindow" parameter that must be set to true. It will only respond when that's the case, and everything else goes through the normal view routing. Because the project uses region management, there is no conflict because these views will not be routed to specific regions. First, if the parameter doesn't exist, the method simply returns. Note the use of the Jounce extension methods that conveniently cast the parameter to a specific type:

public void HandleEvent(ViewNavigationArgs publishedEvent)
{
    var parms = publishedEvent.ViewParameters;
    if (!parms.ContainsKey("OpenInWindow") ||
        !parms.ParameterValue<bool>("OpenInWindow"))
    {
        return;
    }
}

Next, the router is used to get the view model that is mapped to the view, then spin up a non-shared instance of the view and view model. This allows multiple instances to be created and therefore supports multiple windows with the same view/view model combination. The method to get the view takes an object for a parameter that it will set to the data context of the view. Jounce is smart enough to recognize when that object is a Jounce view model, and will take the additional steps of wiring in visual states and calling the InitializeVm and ActivateView methods on the view model. Notice that the parameters are passed into the view model as well - Jounce will pass these in when it attaches the view model to the view.

var viewModelTag = Router.GetViewModelTagForView(
    publishedEvent.ViewType);
var viewModel = Router.GetNonSharedViewModel(viewModelTag);
var view = Router.GetNonSharedView(
    publishedEvent.ViewType,
    viewModel,
    publishedEvent.ViewParameters 
    as Dictionary<string, object>);

Finally, the window is opened with the view set as the content:

new Window
        {
            Title = parms.ParameterValue<string>("WindowTitle"),
            Width = parms.ParameterValue<int>("WindowWidth"),
            Height = parms.ParameterValue<int>("WindowHeight"),
            Content = view,
            Visibility = Visibility.Visible
        };

That's all there is to it. The controller must be imported somewhere to begin listening for events. Then you can simply export the view with a tag like [ExportAsView("MyView")] and publish the navigation using the Jounce extension methods to turn the view tag into a navigation event and add parameters:

var window = "MyView".AsViewNavigationArgs()
    .AddNamedParameter("OpenInWindow", true)
    .AddNamedParameter("WindowTitle", "My View Title")
    .AddNamedParameter("WindowHeight", 300)
    .AddNamedParameter("WindowWidth", 600);
EventAggregator.Publish(window);

Of course you can get even more clever with how you obtain the title or set the sizes, and now opening a child window is not only as easy as publishing an event, but also fully testable in your view models because you can mock the controller for testing.This technique is demonstrated in detail in the chapter about OOB applications in Designing Silverlight Business Applications: Best Practices for Using Silverlight Effectively in the Enterprise (Microsoft .NET Development Series).

Jeremy Likness

Tuesday, October 11, 2011

Quick Tip: Design-Time Views for Regions

If you've worked with the Region Management pattern before, one source of frustration can be the lack of a design-time view. While you can compose individual views to be designer-friendly, the aggregate views that mark regions often end up devoid of anything useful. A simple little trick, however, can change that.

You may be familiar with creating design-friendly view models, but the extensions for Blend work the same with views. For example, consider a main page that has a layout divided into regions that are marked by content controls, like this:

<ContentControl  VerticalAlignment="Center" Margin="20 0 0 0" region:ExportAsRegion.RegionName="SortRegion"/>

There may be one or many views that can fit in the content control, but having it completely empty just doesn't make a good design experience as you cannot tell how components will fit together. With just a little tweak, however, you can change that. Consider this approach instead:

<ContentControl  d:DataContext="{d:DesignInstance Views:SortView,IsDesignTimeCreatable=True}" Content="{Binding}"
VerticalAlignment="Center" Margin="20 0 0 0" region:ExportAsRegion.RegionName="SortRegion"/>

This simple change has done a few things. First, it binds an instance of a reference view to the data context of the control. The binding will only be invoked in the designer and will not be impacted during runtime. Second, the content control is set to the binding, which is the view in the designer, causing it to render the content. With just that simple tweak you now can see how a view will fit into the region!

The view is created directly and is not recursively wired into the design-time experience, so any design-time data within the referenced view will not appear. To get around that you'll need to create a mock view instead and create an instance of that. If the view isn't activated right away, the content control will end up hosting whatever it inherits from the data context of its parents, which may have undesired side effects, but in most cases you will likely activate the view that routes to the region before any artifacts are visible to the end user. If you need to, you can also manually set the data context (the "real" data-context, not the design-time one) to {x:Null} to prevent it from inheriting anything at runtime.

Finally, there is always the option to use a custom markup extension if you really need a more robust view. For me, the trade-off of making a simple tweak is worthwhile.

Here's the design-time view of a small control, showing sample data:

Here's the main page with nothing but regions and design-time views. While the design-time data isn't wired, the views fall into place and I can get a sense of the overall layout (click to see a larger image):

And finally this is the same application at runtime:

So now you don't have to suffer from regions turning their backs on your designer. Enjoy!

Jeremy Likness

Friday, October 7, 2011

Using Visual States to Set Focus on a Control

A common problem encountered in Silverlight applications is how to set the focus properly when transitioning between controls. For example, in the reference application I'm writing for the book Designing Silverlight Business Applications there is the option to either edit an existing record or create a new one. The result of clicking the appropriate button is a panel overlay that zooms into view. Obviously, it makes sense once the panel is rendered to set the focus to the first field so the user can simply begin entering information.

If the control was managed by adding and removing it from the visual tree, the solution would be simple. This is the perfect example I have of not resorting to exotic solutions to a simple problem. The focus problem is not a business problem, it is a user interface problem. It's perfectly valid to wire a loaded event to the control, name the field that requires focus, and call the Focus method on that field when the parent control is loaded. This is an example where even with MVVM, it just doesn't make sense to solve the problem in the view model when it is so simple to add the code-behind. This doesn't change my ability to have a design-time data or to test my view model. It also works well because if I reuse the same control, the loaded event will still fire every time it is added back to the visual tree.

I mentioned this would be simple if the control was managed based on adding and/or removing it ... but that's not how I manage it. Instead, I use the Jounce framework to position it in a region and show or hide it by changing the visual state. What's nice about this is that I can have a state based on the action that the view model is invoked with, for example "New" for a new item, "Edit" to edit an existing item, and "Closed" when the view should go away. These actions are all handled by setting the visual state, so there is no convuluted logic to set visibility properties or wire any other logic. The visual state is wired as a delegate by Jounce, so when I test my view models I can simply mock the delegate and confirm the transition is set without actually loading a view. I can also set different types of animations, such as zooming the control from the "new" button for one mode and having it fade in from the grid in the other mode.

The problem with this approach is that the control never leaves the visual tree. Everything is managed by visual states, so the "Closed" state simply sets the visibility to collapsed, which keeps it in the visual tree but just ignores any rendering or hit testing logic. So how can I effectively set the focus based on a visual state transition?

I've read some interesting solutions that create a special "reverse behavior" that wires to the control and can be data-bound to a property on the view model. The view model can simply set the property and the data-binding will force the action to happen. Personally, I think this is a fine approach but I don't see the need for my view model to have to understand that part of the UI concern is setting a focus. I wanted to approach it from a UI-centric view and only involve the view model if needed.

It turns out the solution is quite simple. All I need to do is create a control to help manage the focus. Call it the "focus helper" and start with a dependency property that points to another control:

public class FocusHelper : Control
{
    public static readonly DependencyProperty TargetElementProperty =
        DependencyProperty.Register(
            "TargetElement",
            typeof (Control),
            typeof (FocusHelper),
            null);
       
    public Control TargetElement
    {
        get { return (Control) GetValue(TargetElementProperty); }
        set { SetValue(TargetElementProperty, value); }            
    }

}

The reason this helper derives from Control is because I want to be able to place it inside a grid. The next thing I'll do is expose a property to set the focus. First, the details of the property itself:

public static readonly DependencyProperty SetFocusProperty =
    DependencyProperty.Register(
        "SetFocus",
        typeof(bool),
        typeof(FocusHelper),
        new PropertyMetadata(false, FocusChanged));

public bool SetFocus
{
    get { return (bool) GetValue(SetFocusProperty); }
    set { SetValue(SetFocusProperty, value);}
}

You'll notice that I set the property metadata to invoke a callback whenever the property changes. In that call, I want to see if I have a control set. If I do, then I'll simply call the Focus method for that control, then reset the flag back to false so it can fire again if it is set in the future;

private static void FocusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var targetElement = d.GetValue(TargetElementProperty) as Control;
    if (targetElement == null || e.NewValue == null || (!((bool) e.NewValue)))
    {
        return;
    }
    targetElement.Focus();
    d.SetValue(SetFocusProperty, false);
}

Now I've got a handy control that will fire the focus method on a target element whenever its own SetFocus property is set to true. It's easy to wire it to whatever input control I want, in this case a TextBox called "Title":

<Behaviors:FocusHelper x:Name="FocusHelper" 
       TargetElement="{Binding ElementName=Title}"/>
<TextBox x:Name="Title" Text="{Binding Title,Mode=TwoWay}"/>

Doing the binding this way also makes it Blend-friendly. If the focused element needs to change, the designer will be able to click on the binding and target the property all through the designer without touching the Xaml. The last step is to update the visual state. When the control is shown, the visibility is set to "Visible." I simply add another entry to the storyboard that sets the focus property to "true":

<VisualState x:Name="New">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" 
                     Storyboard.TargetProperty="(UIElement.Visibility)">
            <DiscreteObjectKeyFrame KeyTime="0:0:0">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Visible</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusHelper" 
                      Storyboard.TargetProperty="(FocusHelper.SetFocus)">
                <DiscreteObjectKeyFrame KeyTime="0:0:0">
                    <DiscreteObjectKeyFrame.Value>
                        <System:Boolean>true</System:Boolean>
                    </DiscreteObjectKeyFrame.Value>
                </DiscreteObjectKeyFrame>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
</VisualState>

And that's it ... now any time I set the control to a visual state that makes it appear, the focus is set to the input control of my choice and the user is able to start typing or tabbing between fields right away. To learn more about the application, order your copy of Designing Silverlight Business Applications and read how it was constructed with access to the full source code.

Jeremy Likness