Tuesday, September 29, 2009

Decoupled ChildWindow Dialogs with Prism in Silverlight 3

A common user interface component is the confirmation or message box, which is often presented as a dialog returns a boolean (OK/Cancel). There are a variety of ways to achieve this, but how can you decouple the implementation of the popup from the request itself? This is necessary, for example, for unit testing when you may not have a UI available. This article demonstrates one solution using Silverlight and the Composite Application Guidance library, also known as Prism.

The first piece we are going to build is a payload that allows us to deliver the popup message. The payload looks like this:

public class MessageBoxPayload
{
    public object DataContext { get; set; }

    public bool AllowCancel { get; set; }

    public string Title { get; set; }

    public string Message { get; set; }

    public Action<MessageBoxPayload> ResultHandler { get; set; }

    public bool Result { get; set; }

    private MessageBoxPayload()
    {            
    }

    public static MessageBoxPayload GeneratePayload(object dataContext, bool allowCancel, 
        string title, string message, Action<MessageBoxPayload> resultHandler)
    {
        MessageBoxPayload retVal = new MessageBoxPayload {AllowCancel = allowCancel, Title = title, Message = message, 
            ResultHandler = resultHandler,
        DataContext = dataContext};
        return retVal; 
    }
}

Because we are implementing this completely decoupled, we cannot make any assumptions about state. Therefore, the payload carries a data context that can be passed back to the requestor. This is done using an Action of the payload type, allowing the requestor to provide a method to get called when the dialog closes.

Next, we'll define the event we use to request the message box. That is based on the event mechanism supplied by Prism:

public class MessageBoxEvent : CompositePresentationEvent<MessageBoxPayload>
{
}

Notice that we derive from the CompositePresentationEvent. This is a base class for all events that will participate in the event aggregator service. Now that we have our payload and our service defined, we can easily begin to pubish to the event. If you had a data grid with a delete button, the event would look something like this (for simplicity's sake I'm not using DelegateCommand):

private void Button_Delete_Click(object sender, System.Windows.RoutedEventArgs e)
{
    Button src = e.OriginalSource as Button;
    if (src != null)
    {
        _eventService.GetEvent<MessageBoxEvent>().Publish(
            MessageBoxPayload.GeneratePayLoad(src.DataContext, true, "Please Confirm", "Are you sure you wish to delete this item?",
                                      DeleteItem));
    }
}      

public void DeleteItem(MessageBoxPayload payload)
{
    if (payload.result) 
    {
       MyItem item = payload.DataContext as item;
       Delete(item);
    }
}

For unit tests, you can now simply build an object that subscribes to the delete event and returns the desired results.

As you can see, the delete click wraps the message into a payload and publishes the event. A delegate is provided to call back to "DeleteItem" with the result of the dialog. If the user confirmed, then the data context is used to pull the entity for the given row and the delete command is performed.

The _eventService is defined like this:

...
private readonly IEventAggregator _eventService;
...

Because IEventAggregator is supplied as a parameter in the constructor for the view, it is automatically wired in by the dependency injection framework. There are two steps required to get the event aggregator to your objects.

First, in your bootstrapper, you'll want to register a single instance:

protected override void ConfigureContainer()
{
    base.ConfigureContainer();

    // provide a reference to the event aggregator
    Container.RegisterInstance<IEventAggregator>(Container.Resolve<EventAggregator>());

}

Notice that I use "resolve" to get the instance. This is good practice when using a dependency injection/inversion of control framework. When you new your objects, you must select the appropriate constructor and inject the appropriate values. By calling Resolve you ask the container to read the signature of the constructors and provide implementations based on your current configuration and registrations.

The second step is to inject the service to the appropriate handler for the popup. Let's focus on implementing the actual popup. The easiest way to handle common infrastructure items is to create a common module. That module can contain elements that are shared between projects. First, we'll create a new child window and call it "Popup." The XAML looks like this (in my case, the project is Modules.Common and the Popup is in a subfolder called Views).

<controls:ChildWindow x:Class="Modules.Common.Views.Popup"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="Auto" Height="Auto" 
           Title="{Binding Title}">
    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Text="{Binding Message}" FontFamily="Arial" FontSize="12" TextAlignment="Center" Grid.Row="0"/>
        <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

The main thing to note is the use of Auto to ensure the dialog resizes based on the content. Now for the code behind.

public partial class Popup
{   
    public Popup()
    {
        InitializeComponent();
    }

    public Popup(MessageBoxPayload payload)
    {
        InitializeComponent();
        DataContext = payload;
        CancelButton.Visibility = payload.AllowCancel ? Visibility.Visible : Visibility.Collapsed;
    }                

    private void OKButton_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = true;
        MessageBoxPayload result = (MessageBoxPayload) DataContext;
        result.Result = true; 
        result.ResultHandler(result);
    }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = false;
        MessageBoxPayload result = (MessageboxPayload)DataContext;
        result.Result = false;
        result.ResultHandler(result);
    }
}

The key here is that we have a constructor that takes in the payload and sets the data context, then sets the visibility of the cancel button. Then there are events bound to the buttons that will set the appropriate result then call the handler specified for the dialog close event.

One confusing topic here may be how to get the view into the application. If this is truly a decoupled module, how will it "know" about the regions you've defined? Furthermore, even if you iterated the region collection and injected it to the first one you found, you will find a goofy, empty popup window just hanging out. Not exactly what we want! In order to manage this popup, we'll use a controller.

The popup controller looks like this:

public class PopupController
{
    public PopupController(IEventAggregator eventService)
    {
        eventService.GetEvent<MessageBoxEvent>().Subscribe(PopupShow);
    }

    public void PopupShow(MessageBoxPayload payload)
    {
        Popup popupWindow = new Popup(payload);
        popupWindow.Show();          
    }
}

Now we can set up our module to invoke the controller.

public class CommonModule : IModule
{               
    private readonly IUnityContainer _container;

    public CommonModule(IUnityContainer container)
    {
        _container = container;
    }

    public void Initialize()
    {
        _container.RegisterInstance(_container.Resolve(typeof (PopupController)));         
    }
}

Notice we aren't registering with a region. Instead, we simply resolve a single instance of the controller. This will subscribe to the popup event. Because we use the container to resolve the controller, the container will automatically reference the EventAggregator and inject it into the constructor. Last but not least, this module simply needs to get registered with the module catalog:

protected override IModuleCatalog GetModuleCatalog()
{
    ModuleCatalog catalog = new ModuleCatalog();
    catalog.AddModule(typeof (MySpecificModule));
    catalog.AddModule(typeof (CommonModule)); 
    return catalog; 
}

Again, because we registered the EventAggregator earlier, it will pass the container along to the module and inject the aggregator into the controller. Now, when we publish the event, a nice child window will appear for us:

Silverlight popup

Of course, you can extend this to have the controller manage multiple windows, customize the button text or add nice images as well. This is just one of the many ways use of dependency injection and the event aggregator pattern can help the need (give me a response) from the implementation (show a popup) and provide an easy way to reuse components across applications.

Jeremy Likness

14 comments:

  1. VERY nice article!
    very clear and deliberate. great!

    one thing though:
    in your controller class, inside "PopupShow()" you are making a new instance of the Popup each time: "Popup popupWindow = new Popup(payload);"
    seems to me it this is risky - you could end up with lots of instances of it living in the background without them being destroyed & garbage collected.
    don't you think it is a better idea to hold only ONE instance of this? instantiate it in the controller's constructor then just show/hide the same instance.

    ReplyDelete
  2. It's a question of your approach. I personally don't think mistrust of the garbage collector is a valid reason to switch to a singleton pattern. In this case, it could be argued that Silverlight has only one UI thread, and therefore only one instance of the dialog is needed. However, if we properly unregister our events and remove references to the dialog once it's done, it should be cleaned up by the garbage collector. I'm not worried about interim instances because the whole point of garbage collection is to clean these up when there is memory pressure.

    Again, I understand your point, but using a singleton because it makes sense for various reasons is one thing. Making a singleton just because we're worried about garbage collection doesn't make sense to me ... we must trust the garbage collector and then take steps to make sure we've built our classes to comply with how it works.

    ReplyDelete
  3. and another:
    a different approach to managing the popup, instead of the PopupController we could write a service:
    theContainer.RegisterType(new ContainerControlledLifetimeManager());

    ReplyDelete
  4. Yes! That's exactly what I wrote about in my more recent post on the simple dialog service:

    Simple Dialog Service in Silverlight

    ReplyDelete
  5. and another:

    i think doing:
    _container.RegisterInstance(_container.Resolve(typeof (PopupController)));
    is a bit redundant.

    you can just go:
    _container.Resolve();

    ReplyDelete
  6. oops, seems like something was removed.
    should be:
    _container.Resolve\PopupController\();

    ReplyDelete
  7. This may be a stupid question but what is the "PopupEntity" class?

    ReplyDelete
  8. It was an ugly typo from transposing names, now fixed. Thanks for the catch!

    ReplyDelete
  9. I changed it earlier today in my code to MessageBoxPayload because that seemed to fit the best. However, I didn't want to make any assumptions.

    Thanks for the quick response!

    ReplyDelete
  10. Thanks. I'm allowing 3rd party modules that'll all load into a custom window that will allow restrictive access to the shell app. This helps with the code architecture for that.

    ReplyDelete
  11. Jeremy, thank you for the post. Please help me with a clarification as I'm working on a project and am going to begin implementing popups and I would like to do it correctly. I am using Prism but with WPF.

    In my app, I have a form bound to my viewmodel. This form will be able to add child records using different popup windows (editable windows) for each. So, there will be at least these forms: Labor, Equipment, Production, etc.

    Based on your article, am I going to have to create for every popup window, their own payload, controller and IModule, classes or is there a more efficient way?

    Thank You.

    ReplyDelete
  12. Jeremy, Thanks forsuch a nice article. But in my application I am using MEF instead of Unity. Can you tell me where in your code I need to make change to make it compatible with MEF.

    I am quite new to Prism stuff. Please explain in detail.

    Thanks,
    Mohit Goel.

    ReplyDelete
  13. Hi Jeremy,
    I am little confused. Do you have sample project. That will be great.

    Thank you.

    ReplyDelete