Click here to Skip to main content
15,122,149 members
Articles / Programming Languages / C#
Technical Blog
Posted 29 Sep 2009

Stats

22.5K views
8 bookmarked

Decoupled ChildWindow Dialogs with Prism in Silverlight 3

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
29 Sep 2009CPOL4 min read
Decoupled ChildWindow Dialogs with Prism in Silverlight 3

A common user interface component is the confirmation or message box, which 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:

C#
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 PopupEntity 
		{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:

C#
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 publish 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):

C#
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:

C#
...
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:

C#
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).

XML
<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.

C#
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:

C#
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.

C#
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:

C#
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 to use 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jeremy Likness
Program Manager Microsoft
United States United States
Note: articles posted here are independently written and do not represent endorsements nor reflect the views of my employer.

I am a Program Manager for .NET Data at Microsoft. I have been building enterprise software with a focus on line of business web applications for more than two decades. I'm the author of several (now historical) technical books including Designing Silverlight Business Applications and Programming the Windows Runtime by Example. I use the Silverlight book everyday! It props up my monitor to the correct ergonomic height. I have delivered hundreds of technical presentations in dozens of countries around the world and love mentoring other developers. I am co-host of the Microsoft Channel 9 "On .NET" show. In my free time, I maintain a 95% plant-based diet, exercise regularly, hike in the Cascades and thrash Beat Saber levels.

I was diagnosed with young onset Parkinson's Disease in February of 2020. I maintain a blog about my personal journey with the disease at https://strengthwithparkinsons.com/.


Comments and Discussions

 
GeneralNice I have one query Pin
Sacha Barber12-May-10 10:19
mvaSacha Barber12-May-10 10:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.