Introduction
I have written and commented on many WPF patterns to include multithreading and still regularly see others provide information on the same topic without the full scope of WPF features. Particularly in the case of multithreading not talking about how Reactive Extensions can be used to easily invoke work on the Dispatcher with multithreading. So in this demo we're going to add log messages to the UI in WPF through Reactive Extensions.
You may be wondering, why show logs in WPF at all? Isn't this just pushed back to the log file or database and not worried about unless there's a problem? Why not just have the user open a log reader instead?
Outside of being a really simply business example and cross-cutting concern for a demo, some applications provide a lot of value by having the ability to show some sort of message to the users. In my experiences with the financial industry and in engineering companies the users have always demanded being able to see more information about what's going on in the system. Except that data tends to be way too verbose to continually show in the application.
Being able to view and manage what you see inside the application is a lot easier than having to deal with an external source. You could make a completely seperate model called "UserMessage" to relay finer grain information to the user; however, there are still cross-cutting concerns logging needs to take care of such as how to display errors to the user and there will be a lot of duplication between them.
There's a ton of other fundamental WPF framework architecture information/examples inside the demo too. For example, use of a contracts assembly to properly create and implement interfaces with Dependency Inversion. So check out the code and start playing around with more features!
Background
This project walks through beginner and intermediate examples, but the reader should be at least familiar with:
- PRISM
- IOC
- MEF
- Reactive Extensions
- Multi-threading
Using the code
Let's look at the solution explorer first to understand the structure for proper Dependency Inversion:
The key to it all is a contracts assembly. This is where all of the global level models and interfaces will go. It follows Martin Fowler's recommendations on enterprise archtecture and the models are meant to be relatively dumb objects. This is a whole other conversation on architecture so just trust me on this for now...
The ILoggingService
interface is therefore in the contracts and the implementation on the service is in the NSanity.Logging.Services
assembly. What makes this true dependency inversion is that nothing consuming the ILoggingService
has an assembly dependency on the NSanity.Logging.Services
assembly.
NOTE: Before running the code yourself you must build the entire solution. Since there are no direct dependencies on the NSanity.Logging.Services project just clicking 'Start' won't cause the project to build.
In this demo the ILoggingService
is available through IOC - specifically MEF. You could use any creational pattern to get the implementation, but here's how it looks in MEF:
[Export(typeof(ILoggingService))]
[Export(typeof(ILoggerFacade))]
public class LoggingService : ILoggingService, ILoggerFacade
{ ... }
[Export]
public class LogFeedViewModel : BaseViewModel
{
[Import]
private ILoggingService _logger;
private ObservableCollection<Log> _logs;
public ObservableCollection<Log> Logs
{
get
{
if (_logs == null)
{
_logs = new ObservableCollection<Log>();
_logger
.LogFeed
.ObserveOnDispatcher()
.Subscribe(async (l) =>
{
await OnLogRecieved(l);
});
}
return _logs;
}
}
...
}
The ILoggingService
is implemented by the class and exported. This will register the class with the MEF container.
Next the interface needs to be imported to be consumed. In order to import the class doing the importing must be intialized from the container. That means in examples you have a model or view model initialized directly the imports will still be null.
NOTE: If that doesn't make sense you can blow past that or hit up the MSDN articles on MEF.
So now we have our logging service, the view model interacting with it, and all that's left is actually writing log files. This is where we're going to look at the Observable
class and their interaction with the Dispatcher
.
[Export]
public class ShellViewModel
{
[Import]
private ILoggingService _loggingService;
[Import]
private LogFeedViewModel _logFeed = null;
public LogFeedViewModel LogFeed
{
get { return _logFeed; }
}
public ShellViewModel()
{
Observable
.Interval(TimeSpan.FromSeconds(1))
.Subscribe(_ =>
{
_loggingService.Log(new Log("Debug log message created in ShellViewModel"));
});
Observable
.Interval(TimeSpan.FromSeconds(5))
.Subscribe(_ =>
{
_loggingService.Log(new Log("Erro log message created in ShellViewModel", LogSeverity.Error, new Exception("Bad bad things happened.")));
});
}
For testing purpose it's simply going to use the Observable.Interval
to generate a message every one second and five seconds to show two different severities.
In the view...
<Window x:Class="NSanity.Wpf.LoggingDemo.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ShellView" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Button Content="Clear"
Command="{Binding LogFeed.ClearLogsCommand}"/>
<DataGrid Grid.Row="1"
ItemsSource="{Binding LogFeed.Logs}"/>
</Grid>
</Window>
It's super simple for now... Just update the grid and it's using auto-generated columns. The key is back in the LogFeedViewModel
by using ObserveOnDispatcher
the underlying mechanics are using the Task
class to join to the Dispatcher
for us not only reducing code and making it simple but also helping use best practices. This means we're adding to the Logs
collection on the Dispatcher
.
There's a simple example of commands using the PRISM DelegateCommand
and async
/await
to show hooking up commands. It will call Task.Delay
to show the UI is still responsive even while the command execution is blocked.
Voila!
Points of Interest
There's a whole lot to explore in this demo. Those wanting some basic examples of high level architecture concepts will find them here. Dig around and by all means expand this for your own personal use.
I called this a framework but it's the skeleton for a framework really... The overall ability to re-use this code as a framework in any of your company applications is definitely there though. You'll want to add things like filters for log severity levels, your own permanent log storage (i.e. log4net), etc.
You can fancy everythiing up obviously. I prefer DevExpress for WPF controls and there is some great grid control functionality as well as controls for toasters.