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:
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:
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
):
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();
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:
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.