Contents
Introduction
Calcium is a WPF composite application toolset that leverages the Composite Application Library. It provides much of what one needs to rapidly build a multifaceted and sophisticated modular application.
In part one of this series, we explored some of Calcium’s core infrastructure, including module management, region adaptation, and the Bootstrapper
. Now, we will examine the messaging system, and take a look at two other modules, namely the WebBrowser
module and the Output
module.
Figure: Calcium with the Web browser and Output modules visible.
In the first article, we saw that Calcium consists of a client application and server based WCF services, which allow interaction and communication between clients. Out of the box, Calcium comes with a host of modules and services, and an infrastructure that is ready to use in your next application.
We've got a lot of ground to cover. For that reason, I've decided to break this article up into a series of three, maybe four articles.
- Introduction to Calcium, Module Manager
- Message Service, WebBrowser module, Output Module (this article)
- File Service, View Service, Rebranding Calcium
- TBA
In this article, you will learn how to:
- build a location agnostic messaging system;
- use WCF custom headers to identify specific client application instances;
- create web browser and output window modules for Calcium;
- inject
RoutedCommand
handlers into the shell (main window).
Some of the contents in this series of articles are not at an advanced level, and will suit even those that are new to Prism. While others, such as the messaging system, will suit more advanced readers. Hopefully, there are things here to learn for everyone.
These series of articles are, in some respects, an introduction to some areas of Prism. Although, if you are completely new to Prism, you may find yourself somewhat overloaded at times, and I would recommend taking a look at some beginner Prism articles before you tackle Calcium, such as Jammer's Introduction or the Technical Concepts on MSDN.
Location Agnostic Message Service
When developing an application, clearly it’s prudent to have uniformity in the manner certain tasks are carried out. An example of such a task is displaying common dialog boxes. But wait, if you think this section is just going to be about an abstracted dialog box system, it isn't. That would be far too boring. While Calcium does provide a common dialog system, it also allows us to display a dialog to the user from the server during any WCF call! Moreover, it allows us to consume the same API on the client and the server. We don't need to worry about interpreting the result of a WCF call in the client. This means we are able to interact with the user directly from anywhere, without having to know where our business logic is executing, i.e., client or server.
Out of the box, Calcium comes with a number of IMessageService
implementations. There is a client implementation for WPF, another client-side implementation for command line driven applications, and a server-side implementation that sends messages back to the client via a callback and leverages the client-side IMessageService
implementation.
Firstly, I want to provide you with an overview of the client-side message service, and then we will examine how it is leveraged from the server-side to provide the location agnosticism.
Client-side Message Service Implementation
Obviously, it’s unwise for each member of a development team to be creating his or her own dialogs for simple tasks such as asking the user a closed ended question (a Yes/No question box). We would end up with lots of duplication, and that degrades maintainability. If we decide to change the caption in the dialogs across the board, it is rather more difficult if dialogs are scattered throughout the project. Likewise, if we wish to port the application from WPF to Silverlight, or even to a command line interface (think Powershell or mobile applications), it’s great to be able to swap out the implementation for any given scenario. Clearly, an abstracted layer is in order.
The Message Service has various overloads which allow errors, captions, and messages to be specified.
Let’s take a look at the IMessageService
interface and client-side implementations.
Figure: IMessageService
allows us to interact with the user in a UI agnostic manner.
The Message Service allows for a Message Importance level to be specified. This allows for a threshold noise level to be specified by the user. If the MessageImportance
is lower than the user’s preference, then the user won't be bothered with the message. In a later version of Calcium, we shall see a Preferences Service for specifying the preferred level.
Figure: MessageService
and CommandLineMessageService
both override the ShowCustomDialog
method of MessageServiceBase
to cater for their particular environments.
By applying variation through merely overriding the ShowCustomDialog
method, it also makes it very easy to mock the MessageServiceBase
class for testing.
Our client-side WPF implementation channels all messaging requests through the ShowCustomDialog
method as shown in the following excerpt:
public class MessageService : MessageServiceBase
{
public override MessageResult ShowCustomDialog(string message, string caption,
MessageButton messageButton, MessageImage messageImage,
MessageImportance? importanceThreshold, string details)
{
if (importanceThreshold.HasValue &&
importanceThreshold.Value < MinumumImportance)
{
return MessageResult.OK;
}
if (MainWindow.Dispatcher.CheckAccess())
{
var messageBoxResult = MessageBox.Show(MainWindow, message, caption,
messageButton.TranslateToMessageBoxButton(),
messageImage.TranslateToMessageBoxButton());
return messageBoxResult.TranslateToMessageBoxResult();
}
MessageResult result = MessageResult.OK;
MainWindow.Dispatcher.Invoke((ThreadStart)delegate
{
var messageBoxResult = MessageBox.Show(MainWindow, message, caption,
messageButton.TranslateToMessageBoxButton(),
messageImage.TranslateToMessageBoxButton());
result = messageBoxResult.TranslateToMessageBoxResult();
});
return result;
}
static Window MainWindow
{
get
{
return UnitySingleton.Container.Resolve<IMainWindow>() as Window;
}
}
}
I have created various extension methods for translating between the native WPF enum
s MessageBoxButton
, MessageBoxImage
, and MessageBoxResult
. Why go to all of this trouble? At first glance, it resembles an antipattern. The reason is, in fact, that there are differences in these enum
s in WPF and Silverlight, and this allows us to cater for both without duplication.
I am considering expanding the service to support message details, and perhaps reducing the number of overloads with a reference type Message
parameter. Another improvement would be to implement a Don't show again checkbox system. I leave that for a future incarnation. I hope to use Karl Shifflet’s excellent Common TaskDialog project WPFTaskDialogVistaAndXP.aspx (with Karl's permission).
Server-side Message Service
We've looked at the basic client-side implementation of IMessageService
, but now things are going to get a little more interesting. With Calcium, we have the capability to consume the IMessageService
in the same manner on both client and server. To accomplish this, we use a WCF custom header and a duplex service callback.
The demonstration application contains a module called the MessageServiceDemoModule
. The view for this module contains a single button that becomes enabled when the CommunicationService
has connectivity with the server.
The following excerpt shows code from a WCF service, which is executed asynchronously. The user is presented with dialogs on the client-side. Remember, this code executes server-side!
public class DemoService : IDemoService
{
public void DemonstrateMessageService()
{
var messageService = UnitySingleton.Container.Resolve<IMessageService>();
ThreadPool.QueueUserWorkItem(delegate {
messageService.ShowMessage(
"This is a synchronous message invoked from the server. "
+ "The service method is still executing and waiting for your response.",
"DemoService", MessageImportance.High);
bool response1 = messageService.AskYesNoQuestion(
"This is a question invoked from the server. Select either Yes or No.",
"DemoService");
string responseString = response1 ? "Yes" : "No";
messageService.ShowMessage(string.Format(
"You selected {0} as a response to the last question.", responseString),
MessageImportance.High);
});
}
}
In the above excerpt, we retrieve the IMessageService
instance from the Unity container. This is done in the same manner as we would on the client! Being location agnostic allows us to move business logic far more easily. I have placed the Message Service calls here inside a QueueUserWorkItem
delegate just to demonstrate the independence of the Message Service. The WCF call returns almost immediately, yet a child thread will continue to work in the background and will still retain the capability to communicate with the user. Without using a child thread to communicate with the user, we may experience a timeout if the user fails to respond fast enough. Please note that we are required to retrieve the Message Service from the Unity container before the service call completes. Not doing so will raise an exception, as the OperationContext
for the service call will no longer be present.
Figure: Server causes dialog to be presented client-side.
Figure: Question asked from server.
Figure: Response received and echoed back to user.
How it all Works
When we need a service channel, we use our IChannelManager
instance to retrieve an instance. I've discussed the IChannelManager
in other articles, in particular here and here.
In WCF, each WCF service has an independent session state. In other words, WCF services don't share a global session. So, in order to identify the client application instance from any WCF service call, we place a custom header into every service channel we create, as shown in the following excerpt from the ServiceManagerSingleton
and InstanceIdHeader
classes:
public static class InstanceIdHeader
{
public static readonly String HeaderName = "InstanceId";
public static readonly String HeaderNamespace
= OrganizationalConstants.ServiceContractNamespace;
}
We use this static
class to place the header, and also to retrieve the header on the server.
void AddCustomHeaders(IClientChannel channel)
{
MessageHeader shareableInstanceContextHeader = MessageHeader.CreateHeader(
InstanceIdHeader.HeaderName,
InstanceIdHeader.HeaderNamespace,
instanceId.ToString());
var scope = new OperationContextScope(channel);
OperationContext.Current.OutgoingMessageHeaders.Add(shareableInstanceContextHeader);
}
So, when we create the service channel, we add the custom header as shown in the following excerpt:
public TChannel GetChannel<TChannel>()
{
Type serviceType = typeof(TChannel);
object service;
channelsLock.EnterUpgradeableReadLock();
try
{
if (!channels.TryGetValue(serviceType, out service))
{
channelsLock.EnterWriteLock();
try
{
var channelFactory = new ChannelFactory<TChannel>("*");
service = channelFactory.CreateChannel();
AddCustomHeaders((IClientChannel)service);
var communicationObject = (ICommunicationObject)service;
communicationObject.Faulted += OnChannelFaulted;
channels.Add(serviceType, service);
communicationObject.Open();
ConnectIfClientService(service, serviceType);
UnitySingleton.Container.RegisterInstance<TChannel>((TChannel)service);
}
finally
{
channelsLock.ExitWriteLock();
}
}
}
finally
{
channelsLock.ExitUpgradeableReadLock();
}
return (TChannel)service;
}
We cache the channel until it is closed or faults. But as soon as we create a new channel, we add the header to be consumed on the server. We also have duplex channel support, which works in much the same way.
We must talk to the ICommunicationService
in order to create a callback, before we attempt to consume the Message Service on the server-side. This is done automatically by the CommunicationModule
. In fact, the CommunicationModule
polls the server periodically to let it know that it is still alive. It will also detect network connectivity, or lack thereof, and disable or enable the polling accordingly.
void NotifyAliveAux(object state)
{
try
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
return;
}
var channelManager = UnitySingleton.Container.Resolve<IChannelManager>();
var communicationService =
channelManager.GetDuplexChannel<ICommunicationService>(callback);
communicationService.NotifyAlive();
if (!connected)
{
connected = true;
connectionEvent.Publish(ConnectionState.Connected);
}
lastExceptionType = null;
}
catch (Exception ex)
{
Type exceptionType = ex.GetType();
if (exceptionType != lastExceptionType)
{
Log.Warn("Unable to connect to communication service.", ex);
}
lastExceptionType = exceptionType;
if (connected)
{
connected = false;
connectionEvent.Publish(ConnectionState.Disconnected);
}
}
finally
{
connecting = false;
}
}
As an aside, the Communication
Module
itself has no UI. Its purpose is to interact with the CommunicationService
, and to provide notifications to the client when various server-side events take place. One such CompositeEvent
is the ConnectionEvent
, which can be seen in the previous excerpt.
I plan on expanding the Message Service system to allow text input, drop down list selection, and maybe even custom dialogs.
WebBrowser Module
The WebBrowser
module consists of the module itself, a view, and a viewmodel. The view, a class called WebBrowserView
, plays host to a WPF WebBrowser
control, whose Url
property is bound to the ViewModel
, as shown in the following excerpt:
<WebBrowser Name="WebBrowser" wb:WebBrowserUtility.BindableSource="{Binding Url}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
Figure: Screenshot of Calcium with WebBrowser on show.
The WebBrowser
toolbar has a button whose Command is the WebBrowserViewModel.NavigateCommand
and a TextBox
. The NavigateCommand
is actually injected into the shell’s CommandBinding
collection so that it may be invoked when the current view implements the content interface IWebBrowserView
.
This can be seen in action in the WebBrowserModule
, where we associate the WebBrowserViewModel.NavigateCommand
with the content type WebBrowserViewModel
, as shown in the following excerpt:
commandService.AddCommandBindingForContentType<WebBrowserViewModel>(
WebBrowserViewModel.NavigateCommand,
(arg, commandParameter) => arg.Navigate(commandParameter),
(arg, commandParameter) => arg.CanNavigate(commandParameter));
Here, the shell accepts an ICommand
, and when a WebBrowserViewModel
becomes active, it will enable the command according to the CanNavigate
handler.
The following excerpt is from the ICommandService
interface, in which we see the various signatures for associating commands.
void AddCommandBindingForContentType<TContent>
(ICommand command,
Func<TContent, object, bool> executeHandler,
Func<TContent, object, bool> canExecuteHandler)
where TContent : class;
void AddCommandBinding(ICommand command,
Func<bool> executeHandler, Func<bool> canExecuteHandler);
void AddCommandBinding(ICommand command,
Func<bool> executeHandler, Func<bool> canExecuteHandler, KeyGesture keyGesture);
void RegisterKeyGester(KeyGesture keyGesture, ICommand command);
void RemoveCommandBinding(ICommand command);
The WPF routed command infrastructure is useful in scenarios where we have a command target located within the visual tree. Yet with an interface that combines tool views and document views, it can be difficult triggering command handlers, and some controls may behave not as expected. This can be demonstrated when e.g., toolbar buttons are not enabled when the current workspace view is not focused.
In order to address this challenge, we use an ICommandService
method to associate a known content type with a command. The following excerpt shows the AddCommandBindingForContentType
interface definition.
The AddCommandBindingForContentType
has been implemented in the DesktopShell.Commanding.cs as the following excerpt demonstrates:
public void AddCommandBindingForContentType<TContent>(
ICommand command,
Func<TContent, object, bool> executeHandler,
Func<TContent, object, bool> canExecuteHandler)
where TContent : class
{
CommandBindings.Add(new CommandBinding(command,
(sender, e) =>
{
var content = GetSelectedContent<TContent>();
if (content == null)
{
return;
}
e.Handled = executeHandler(content, e.Parameter);
},
(sender, e) =>
{
var content = GetSelectedContent<TContent>();
if (content == null)
{
e.CanExecute = false;
return;
}
e.CanExecute = canExecuteHandler(content, e.Parameter);
}));
}
TabItemDictionary.xaml:
<DataTemplate x:Key="TabHeaderDataTemplate">
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock x:Name="textBlock"
Text="{Binding TabHeader}"
HorizontalAlignment="Left" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" Foreground="#FFFFFFFF" Margin="0,2,0,0"
FontFamily="Arial" FontSize="11" />
<TextBlock Text="*"
Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed,
Converter={StaticResource BooleanToVisibilityConverter}}"
HorizontalAlignment="Left"
Foreground="#FFFFFFFF" Margin="0,2,0,0" />
<Button x:Name="button"
Command="ApplicationCommands.Close" CommandParameter="{Binding Path=View}"
Template="{DynamicResource CloseTabButtonControlTemplate}"
Background="{x:Null}" BorderBrush="{x:Null}" Foreground="{x:Null}"
Width="9" Height="9" Opacity="1" ToolTip="Close"
Margin="8,3,0,0" VerticalAlignment="Stretch" BorderThickness="0"
HorizontalAlignment="Right" />
</StackPanel>
</DataTemplate>
This all comes together in the WebBrowserModule
class, which takes the Workspace region from the RegionManager
and populates it with a WebBrowserView
. We then create a new instance of the WebBrowserToolBar
and place it in the StandardToolBarTray
region of the shell.
[Module(ModuleName = ModuleNames.WebBrowser)]
public class WebBrowserModule : IModule
{
public void Initialize()
{
var regionManager = UnitySingleton.Container.Resolve<IRegionManager>();
var view = new WebBrowserView();
regionManager.Regions[RegionNames.Workspace].Add(view);
string startUrl = "http://wpfdisciples.wordpress.com/";
var viewModel = (WebBrowserViewModel)view.ViewModel;
viewModel.Url = startUrl;
var viewService = UnitySingleton.Container.Resolve<IViewService>();
var toolBarProvider = new WebBrowserToolBar { Url = startUrl };
var toolBar = toolBarProvider.ToolBar;
regionManager.Regions[RegionNames.StandardToolBarTray].Add(toolBar);
viewService.AssociateVisibility(typeof(IWebBrowserView),
new UIElementAdapter(toolBar), Visibility.Collapsed);
var shell = UnitySingleton.Container.Resolve<IShell>();
shell.AddCommandBindingForContentType<WebBrowserViewModel>(
WebBrowserViewModel.NavigateCommand,
(arg, commandParameter) => arg.Navigate(commandParameter),
(arg, commandParameter) => arg.CanNavigate(commandParameter));
}
}
In order to hide and show the toolbar according to content present in the interface, we use the IViewService
, which will hide the toolbar when the current content does not implement IWebBrowser
view, and show it when it does. We will examine View Service in the next article in this series.
The final thing we do in the WebBrowserModule
is to add the command binding to the shell.
The reader may notice that throughout my code I generally avoid using constructor injection. What is constructor injection? Constructor injection is where a Dependency Injection (DI) container, in this case Unity, is used to automatically call a non default constructor and supply it with instances of known types. I choose to avoid it because I have found it to be troublesome. When resolution failures occur, and one has a number of cascading types that are resolved during constructor injection, one may find oneself digging through reams of stacktrace looking for the root cause of the failure. Thus, I normally retrieve the DI container via a singleton. I can't see the value in injecting the container either. To me, it’s just bloat.
Output Module
The Output Module is used to display messages received via the OutputPostedEvent
, which is a Prism CompositePresentationEvent
. CompositePresentationEvent
s are used, along with the IEventAggregator
, to allow modules to subscribe and publish events in a decoupled manner. If you are new to CompositePresentationEvent
s (formally CompositeEvent
), more information can be found here[^].
Figure: Routed events cause messages to be displayed in OuputView
.
This event can be published from anywhere client-side. The OutputViewModel
subscribes to this event, and when a message is received, it adds it to an ObservableCollection
,
as shown in the following excerpt:
class OutputViewModel : ViewModelBase
{
readonly ObservableCollection<OutputMessage> outputMessages
= new ObservableCollection<OutputMessage>();
public ObservableCollection<OutputMessage> OutputMessages
{
get
{
return outputMessages;
}
}
public OutputViewModel(IOutputView outputView) : base(outputView)
{
var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
var outputPostedEvent = eventAggregator.GetEvent<OutputPostedEvent>();
outputPostedEvent.Subscribe(OnOutputPosted);
TabHeader = "Output";
}
void OnOutputPosted(OutputMessage message)
{
OutputMessages.Add(message);
}
}
The View is merely responsible for displaying the messages. We can see how a message can be sent to the Output view in the TextEditorModule
.
var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
var outputPostedEvent = eventAggregator.GetEvent<OutputPostedEvent>();
outputPostedEvent.Publish(new OutputMessage {
Category = outputCategory, Message = fileNameUsed + " opened." });
We first retrieve the IEventAggregator
instance from the DI container. Then the event sync is retrieved using the GetEvent
method. We then publish, or raise, the event, which then causes the handler OnOutputPosted
to be called in the OutputViewModel
.
Conclusion
In this article, we have seen how Calcium can be used to provide a location agnostic messaging system, which allows a set of common dialogs to be displayed to the user from anywhere, be it server-side or client-side. We can consume the same API on the client and the server, allowing us to interact with the user directly from anywhere, without having to know where our business logic is executing. We also explored the implementation of a web browser module, an output module, and we examined how it is possible to inject RoutedEvent
handlers into Calcium’s shell.
In the next article, we shall be taking a look at the File Service, which automatically handles common IO errors, and which has a rather flexible API that I really like. We will also look at the User Affinity Module for providing feedback about other users of the application, and finally the Text Editor Module, which will tie in many of the other features we have been exploring.
We still have a lot to cover, and I hope you will join me for our next installment.
I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.
History