Contents
Well, it's now a couple of days after Christmas here, so it's back to work for us. Now for those that know me, I normally write about WPF and related technologies. I like WPF, but it has been a while since I played with its smaller brother Silverlight. I think the last time I played with Silverlight was way back in 2007, when I wrote this article (good in its day I suppose): Silverlight 1.1 Fun and Games.
So I thought it was about time that I had a look at why Silverlight was getting so much Microsoft press and community attention lately, the future of Silverlight, and all that. I say the future is doing cool work, and that is whatever technology that happens to be. But whatever.
This article started off as a small part of a larger SL4 application that I am making to understand and play with RIA Services, but it kinda morphed a bit, and has now taken on a life all of its own. Well, actually, it is kind of two sentient beings now:
Sentient Being 1: A mini Silverlight class library that aids in MVVM development.
Sentient Being 2: What I was supposed to be doing all along, which was playing with the new SL4 bits. To do this, I have created a SL4 uploader that can upload a file to a folder on a web server, and will optionally show a SL3/4 ChildWindow
of the uploaded file if it is an image file.
The attached demo code targets SL4, and was developed as a VS2010 BETA 2 solution, so you will need the following bits and pieces:
All of which I am using for this article... I make no apologies for this, the new stuff is the good stuff.
As I say, the demo app is just one part of the attached demo code, and basically does this: it is a SL4 application that contains a SL4 control that can upload a file to a folder on a web server, and will optionally show a SL3/4 ChildWindow
of the uploaded file if it is an image file.
It looks like this when run:
The user picks a file:
The file is uploaded (asynchronously), and makes use of an ASP.NET Handler. The BusyIndicator
(Silverlight Toolkit) is shown in a SL3/SL4 ChildWindow
.
And if the uploaded file is an image, it is shown in a Modal ChildWindow
(which the ChildWindow
does not do out of the box), as shown here:
This involves lots of neat stuff, like services, asynchronous delegates, dispatcher contexts, building requests/responses, and using ASP.NET Handlers.
As I say though, the demo app is just one part, the second part (which was a bi-product of the demo code, really) is a small Silverlight MVVM framework that aids in the development of creating Silverlight applications the MVVM way. I have knocked all this up in a matter of days (probably 1 or 2 full days work), so it's not a complete MVVM framework, it does not provide all the functions of my Cinch WPF MVVM Framework, but it is a very good start.
Before I dive into how the demo app (the file uploader) works, I will talk about the mini Silverlight MVVM class library that forms part of the attached demo code.
As I say, I have written what I think is a pretty OK MVVM framework for WPF (Cinch WPF MVVM Framework), and there may be some of you who are like why did he not just add to the Cinch WPF MVVM Framework and make it work for both SL and WPF. Well, for me, there is enough difference between the two to warrant a different framework for each. For example, when I wrote the Cinch WPF MVVM Framework, SL did not have any IDataErrorInfo
support and neither did it have loads of other stuff. It could not even open popup windows.
So there are enough differences to my mind to have two dedicated frameworks. To this end, I have just done the minimum amount of work at creating a workable Silverlight MVVM framework as I thought would be required.
Who knows, in time, I may join the two together, but for now, never the twain shall meet I'm afraid.
So what is covered? Well, that would be the following:
- Commanding
- Messaging
- Services
- Threading Helpers
We will now go through all of these areas in turn, but before we do that, I would just like to say thanks and give credit to the following members of the WPF Disciples:
- Josh Smith / Marlon Grech: For their excellent
Mediator
code
- Laurent Bugnion / Josh Smith: For the
RelayCommand/RelayCommand<T>
code
- Daniel Vaughan: For his Silverlight
UISynchronizationContext
idea
Thanks lads.
Commanding
I am basically using Laurent Bugnion's modified versions (see Laurent's MVVMLight project) of Josh Smith's RelayCommand
and RelayCommand<T>
for this.
A simple example of how to use these in your ViewModel may be as follows:
Step 1: Declare the ICommand
property in the ViewModel.
public ICommand UploadCommand
{
get;
private set;
}
Step 2: Hook up the RelayCommand
/ RelayCommand<T>
.
public ImageUploadViewModel()
{
UploadCommand = new RelayCommand(ExecuteUploadCommand, CanExecuteUploadCommand);
}
...
...
private void ExecuteUploadCommand()
{
}
private Boolean CanExecuteUploadCommand()
{
return true;
}
Step 3: Use the ICommand
property exposed from the ViewModel.
<Button Content="Upload File" Command="{Binding UploadCommand}" />
Messaging
I am re-using Marlon Grech's / Josh Smith's excellent Mediator which is also what my Cinch WPF MVVM Framework uses. In case you did not read my Cinch article series, here is what I said about the Mediator then, and this still applies directly to the attached Silverlight MVVM framework.
Now, I do not know about you, but generally, when I working with the MVVM framework, I do not have a single ViewModel managing the whole shooting match. I actually have a number of them (in fact, we have loads). One thing that is an issue using the standard MVVM pattern is cross ViewModel communication. After all, the ViewModels that form an application may all be disparate unconnected objects that know nothing about each other. However, they need to know about certain actions that a user performs. Here is a concrete example.
Say you have two Views: one with customers and one with orders for a customer. Let's say the orders view was using a OrdersViewModel
and that the customers view was using a CustomersViewModel
, and when a customer's order is updated, deleted, or added, the Customer view should show some sort of visual trigger to alert the user that some order detail of the customer changed.
Sounds simple enough, right? However, we have two independent views run by two independent ViewModels, with no link, but clearly, there needs to be some sort of connection from the OrdersViewModel
to the CustomersViewModel
, some sort of messaging perhaps.
This is exactly what the Mediator pattern is all about. It is a simple light weight messaging system. I wrote about this some time ago on my blog, which in turn got made a ton better by Josh Smith / Marlon Grech (as an atomic pair), who came up with the Mediator implementation you will see in Cinch.
So how does the Mediator work?
This diagram may help:
The idea is a simple one: the Mediator listens for incoming messages, sees who is interested in a particular message, and calls each of those that are subscribed against a given message. The messages are usually strings.
Basically, what happens is that there is a single instance of the Mediator
sitting somewhere (usually exposed as a static property in the ViewModelBase
class) that is waiting for objects to subscribe to it either using:
- An entire object reference. Then any
Mediator
message method that has been marked up with the MediatorMessageSinkAttribute
attribute will be located on the registered object (using Reflection) and will have a callback delegate automatically created.
- An actual Lambda callback delegate.
In either case, the Mediator
maintains a list of WeakAction
callback delegates. Where each WeakAction
is a delegate which uses an internal WeakReference
class to check the WeakReference.Target
for null, before calling back the delegate. This caters for the fact that the target of the callback delegate may no longer be alive as it may have been Garbage Collected. Any instances of WeakAction
callback delegates that point to objects that are no longer alive are removed from the list of Mediator
WeakAction
callback delegates.
When a callback delegate is obtained, either the original callback delegate is called, or the Mediator
message methods that have been marked up with the MediatorMessageSinkAttribute
attribute will be called.
Here is an example of how to use the Mediator in all the different possible manners.
Registering for Messages
Using an explicit callback delegate (this is not my preferred option though)
We simply create the correct type of delegate and register a callback for a message notification with the Mediator
.
public delegate void DummyDelegate(Boolean dummy);
...
Mediator.Register("AddCustomerMessage", new DummyDelegate((x) =>
{
AddCustomerCommand.Execute(null);
}));
Register an entire object, and use the MediatorMessageSinkAttribute attribute
This is my favorite approach, and is the simplest approach in my opinion. You just need to register an entire object with the Mediator
and attribute up some message hook methods.
To register, this is done for you in Cinch, you don't have to do anything. Simply inherit from ViewModelBase
, job done. If you are wondering how this is done, the ViewModelBase
class in Cinch simply registers itself with the Mediator
like this.
Mediator.Register(this);
So any method that is marked up with the MediatorMessageSinkAttribute
attribute will be located on the registered object (using Reflection), and will have a callback delegate automatically created. Here is an example:
[MediatorMessageSink("AddCustomerMessage", ParameterType = typeof(Boolean))]
private void AddCustomerMessageSink(Boolean dummy)
{
AddCustomerCommand.Execute(null);
}
How about notification of messages?
Message Notification
That is very easy to do, we simply use the Mediator.NotifyCollegues()
method as follows:
Mediator.NotifyColleagues<Boolean>("AddCustomerMessage", true);
Any object that is subscribed to this message will now be called back by the list of Mediator
WeakAction
callback delegates.
Services
I guess since I wrote so many UI Services for my Cinch WPF MVVM Framework, I did not find it that hard to write some more for Silverlight. The attached demo code makes use of some Silverlight specific UI services which we will get onto in just a minute.
But first, we need to know how to add the in-use services. For this, I am borrowing Marlon Grech's ServiceLocator implementation which is really as simple as having a ServiceLocator
instance in the ViewModelBase
class, that allows new UI services to be added and removed. Here is the attached SL MVVM ViewModelBase
class code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
namespace SLMiniMVVM
{
public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
private static ServiceLocator serviceLocator = new ServiceLocator();
public static ServiceLocator ServiceLocator
{
get
{
return serviceLocator;
}
}
public T GetService<T>()
{
return serviceLocator.GetService<T>();
}
}
}
And this is how the services are setup in the attached demo code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using SLMiniMVVM;
namespace TestSL4
{
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
ViewModelBase.ServiceLocator.
RegisterService<ISLOpenFileService>(
new SLOpenFileService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLChildWindowService>(
new SLChildWindowService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLMessageBoxService>(
new SLMessageBoxService(), true);
ISLChildWindowService popupVisualizer =
ViewModelBase.ServiceLocator.GetService<ISLChildWindowService>();
popupVisualizer.Register("ImagePopup", typeof(ImagePopup));
}
}
}
As this is the real code. The UI services are setup on App.Startup
, but if we were using a Unit Test to test the ViewModels, we could do something like this in the [Setup]
section (assuming NUnit here) of the Unit Test:
[Setup]
private void setup()
{
ViewModelBase.ServiceLocator.
RegisterService<ISLOpenFileService>(
new TestOpenFileService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLChildWindowService>(
new TestChildWindowService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLMessageBoxService>(
new TestMessageBoxService(), true);
}
So exactly what services are provided in the attached Silverlight MVVM library? Well, the following:
Message Box Service
This works pretty much as described and shown in my Cinch WPF MVVM Framework code / How to use the MessageBox service, and this other Cinch article talks about how to setup unit testing.
It should be noted that due to some current Silverlight restrictions, the WPF service provided by Cinch and this Silverlight MVVM library are different, but the idea is practically the same. Read the attached code comments/example and the links above, and all should be clear enough.
Here is what the attached Silverlight MVVM MessageBoxService
API looks like:
public interface ISLMessageBoxService
{
void ShowError(string message);
void ShowInformation(string message);
void ShowWarning(string message);
CustomDialogResults ShowYesNo(string message);
CustomDialogResults ShowYesNoCancel(string message);
CustomDialogResults ShowOkCancel(string message);
}
Open File Service
This works pretty much as described and shown in my Cinch WPF MVVM Framework code / How to use the Open File service, and this other Cinch article talks about how to setup unit testing.
It should be noted that due to some current Silverlight restrictions, the WPF service provided by Cinch and this Silverlight MVVM library are different, but the idea is practically the same. Read the attached code comments/example and the links above, and all should be clear enough.
Here is what the attached Silverlight MVVM OpenFileService
API looks like:
public interface ISLOpenFileService
{
FileInfo File { get; set; }
IEnumerable<FileInfo> Files { get; set; }
String Filter { get; set; }
Int32 FilterIndex { get; set; }
Boolean Multiselect { get; set; }
bool? ShowDialog();
}
Child Window Service
As I stated right at the start of this article, it has been a while since I used Silverlight, and all sorts of fancy/shiny things are now available. One such thing is the ChildWindow
, which sounds cool. Don't know what I am talking about? Well, have a look at this post, it is quite a good introduction to ChildWindow
: http://www.wintellect.com/CS/blogs/jprosise/archive/2009/04/29/silverlight-3-s-new-child-windows.aspx.
Now, that is all well and good, but we are good software devs, and we do not like code-behind as it is not testable. So we need to think about that, and see if we can achieve a UI service that allows us to show ChildWindow
based Silverlight windows, and also allows us to get a DialogResult
back from it, and also allows us to mimic this behaviour in a unit testing environment.
Now if you go and read the ChildWindow
API documentation and lookup the Show()
method, you will notice that MSDN states this: "Opens a ChildWindow
and returns without waiting for the ChildWindow
to close."
Hmmm, so not only do we have to come up with a cool service to deal with the ChildWindow
, but we also have to deal with the fact that it is non-modal and therefore non-blocking.
Now I don't know about you, but I quite like my popup windows (which, let's face it, the ChildWindow
is, even though it will also get used as a fancy progress indicator (as I do in the attached demo app)) to be Modal, so that I know when a user clicks OK, and I apply the changes they made, and if they click Cancel, I discard their work.
Does that sound do-able with the out of the box ChildWindow
? Well, I don't think so. Is there anything we can do about it? The answer, hell yeah.
So what I am about to show you is a UI Service for dealing with ChildWindow
, that can work as a Modeless (non-blocking service), but will also work as a Modal (blocking) popup window with just a little bit of pixie dust.
Here is what the attached Silverlight MVVM OpenFileService
API looks like:
public interface ISLChildWindowService
{
void Register(string key, Type winType);
bool Unregister(string key);
void Show(string key, object state,
EventHandler<UICompletedEventArgs> completedProc);
void ShowModally(string key, object state, AutoResetEvent are,
EventHandler<UICompletedEventArgs> completedProc);
}
Where the actual Silverlight implementation of the service looks like this:
public class SLChildWindowService : ISLChildWindowService
{
#region Data
private readonly Dictionary<string, Type> _registeredWindows;
#endregion
#region Ctor
public SLChildWindowService()
{
_registeredWindows = new Dictionary<string, Type>();
}
#endregion
#region Public Methods
public void Register(Dictionary<string, Type> startupData)
{
foreach (var entry in startupData)
Register(entry.Key, entry.Value);
}
public void Register(string key, Type winType)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
if (winType == null)
throw new ArgumentNullException("winType");
if (!typeof(ChildWindow).IsAssignableFrom(winType))
throw new ArgumentException("winType must be of type Window");
lock (_registeredWindows)
{
_registeredWindows.Add(key, winType);
}
}
public bool Unregister(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
lock (_registeredWindows)
{
return _registeredWindows.Remove(key);
}
}
public void Show(string key, object state,
EventHandler<UICompletedEventArgs> completedProc)
{
ChildWindow win = CreateWindow(key, state,false,null, completedProc);
if (win != null)
{
win.Show();
}
}
public void ShowModally(string key, object state, AutoResetEvent are,
EventHandler<UICompletedEventArgs> completedProc)
{
ChildWindow win = CreateWindow(key, state, true,are, completedProc);
if (win != null)
{
win.Show();
}
}
#endregion
#region Private Methods
private void DoCallback(ChildWindow sender,
EventHandler<UICompletedEventArgs> completedProc)
{
if (completedProc != null)
{
completedProc(sender,
new UICompletedEventArgs(sender.DataContext, sender.DialogResult));
}
}
private ChildWindow CreateWindow(string key, object dataContext,
Boolean isModal, AutoResetEvent are,
EventHandler<UICompletedEventArgs> completedProc)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
Type winType;
lock (_registeredWindows)
{
if (!_registeredWindows.TryGetValue(key, out winType))
return null;
}
var win = (ChildWindow)Activator.CreateInstance(winType);
win.DataContext = dataContext;
if (dataContext != null)
{
var bvm = dataContext as ViewModelBase;
if (bvm != null)
{
bvm.CloseRequest += ((s, e) => win.Close());
}
}
if (completedProc != null)
{
win.Closed +=
(s, e) =>
{
if (isModal)
{
if (are != null)
{
DoCallback(win, completedProc);
are.Set();
}
else
{
DoCallback(win, completedProc);
}
}
else
{
DoCallback(win, completedProc);
}
};
}
return win;
}
#endregion
}
So first, the easy case. Oh, and for each of these, the following is assumed:
- You have a subclass of
ChildWindow
called ImagePopup
- The Silverlight MVVM
OpenFileService
has been setup in App.Startup
as follows:
ViewModelBase.ServiceLocator.RegisterService<ISLChildWindowService>(
new SLChildWindowService(), true);
ISLChildWindowService popupVisualizer =
ViewModelBase.ServiceLocator.GetService<ISLChildWindowService>();
popupVisualizer.Register("ImagePopup", typeof(ImagePopup));
Non-Modal (Non-blocking)
Showing the ChildWindow
subclass is as easy as this, where we show the popup and setup a callback delegate, where the callback delegate will be called when the ChildWindow
subclass' Closed
event happens, as seen in the Silverlight implementation shown above:
synContext.InvokeSynchronously(delegate()
{
popupVisualizer.Show("ImagePopup", this, (s, e) =>
{
if (!e.Result.Value)
{
msgbox.ShowInformation(
"You clicked Cancel. So this could be used " +
"to delete the File from the WebServer.\r\n" +
"Possibly on a new thread, Instead of showing " +
"this blocking Modal MessageBox");
}
});
});
And to setup the ISLChildWindowService
service in a Unit Test (assuming services are setup as described previously):
[Setup]
private void setup()
{
ViewModelBase.ServiceLocator.
RegisterService<ISLOpenFileService>(
new TestOpenFileService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLChildWindowService>(
new TestChildWindowService(), true);
ViewModelBase.ServiceLocator.
RegisterService<ISLMessageBoxService>(
new TestMessageBoxService(), true);
}
Here is how we might set it up to mimic the user in a unit test:
TestChildWindowService testChildWindowService =
(TestChildWindowService)
ViewModelBase.ServiceProvider.Resolve<ISLChildWindowService>();
testChildWindowService.ShowDialogResultResponders.Enqueue
(() =>
{
PopupDataContext context = new PopupDataContext();
context.SomeValue = 42;
context.SomeOtherValue = "yes mate"
return new UICompletedEventArgs(context, true);
}
);
And just for completeness, here is what the TestChildWindowService
code looks like, which works much the same as the TestMessageBoxService
when you get down to it. It just uses the callback Func<T>
to allow the unit test to setup callbacks to supply the required user mocked responses.
public class TestChildWindowService : ISLChildWindowService
{
#region Data
public Queue<Func<UICompletedEventArgs>> ShowResultResponders { get; set; }
public Queue<Func<UICompletedEventArgs>> ShowModallyResultResponders { get; set; }
#endregion
#region Ctor
public TestChildWindowService()
{
ShowResultResponders = new Queue<Func<UICompletedEventArgs>>();
ShowModallyResultResponders = new Queue<Func<UICompletedEventArgs>>();
}
#endregion
#region ISLChildWindowService Members
public void Register(string key, Type winType)
{
}
public bool Unregister(string key)
{
return true;
}
public void Show(string key, object state,
EventHandler<UICompletedEventArgs> completedProc)
{
if (ShowResultResponders.Count == 0)
throw new Exception(
"TestChildWindowService Show method expects " +
"a Func<UICompletedEventArgs> callback \r\n" +
"delegate to be enqueued for each Show call");
else
{
Func<UICompletedEventArgs> responder = ShowResultResponders.Dequeue();
if (completedProc != null)
{
completedProc(null, responder());
}
}
}
public void ShowModally(string key, object state, AutoResetEvent are,
EventHandler<UICompletedEventArgs> completedProc)
{
if (ShowModallyResultResponders.Count == 0)
throw new Exception(
"TestChildWindowService Show method expects " +
"a Func<UICompletedEventArgs> callback \r\n" +
"delegate to be enqueued for each Show call");
else
{
Func<UICompletedEventArgs> responder =
ShowModallyResultResponders.Dequeue();
if (completedProc != null)
{
completedProc(null, responder());
if (are != null)
{
are.Set();
}
}
}
}
#endregion
}
Modal (Blocking)
As I stated above, the default behaviour for ChildWindow
in Silverlight is non-blocking (presumably as its main intention is to show status information while some long running operation happens... but people will be people, and use it differently, I am and would). So we need to do something a little differently if we want to make the UI block waiting for the ChildWindow
.
It is however still very easy using the attached SLChildWindowService
, and all you have to supply is one more bit of information.
Showing the ChildWindow
subclass is as easy as this, where we show the popup and setup a callback delegate, where the callback delegate will be called when the ChildWindow
subclass' Closed
event happens, as seen in the Silverlight implementation shown above.
But also note that this time, we provide a AutoResetEvent WaitHandle
to allow the calling code to block waiting until the ChildWindow
subclass signals to this exact AutoResetEvent WaitHandle
that it may proceed.
AutoResetEvent are = new AutoResetEvent(false);
synContext.InvokeSynchronously(delegate()
{
popupVisualizer.ShowModally("ImagePopup", this, are, (s, e) =>
{
if (!e.Result.Value)
{
msgbox.ShowInformation(
"You clicked Cancel. So this could be used to " +
"delete the File from the WebServer.\r\n" +
"Possibly on a new thread, Instead " +
"of showing this blocking Modal MessageBox");
}
});
});
are.WaitOne();
Let's have another look at the relevant section of the actual Silverlight ISLChildWindowService
implementation to see how this AutoResetEvent WaitHandle
gets signaled.
if (completedProc != null)
{
win.Closed +=
(s, e) =>
{
if (isModal)
{
if (are != null)
{
DoCallback(win, completedProc);
are.Set();
}
else
{
DoCallback(win, completedProc);
}
}
else
{
DoCallback(win, completedProc);
}
};
}
Testing Setup Differences Between Non-Modal / Modal
There is no difference in how you would setup a test version of this service for Non-Modal and Modal.
Other People's Work
I should point out that after I finished writing up this code and article text, I did a quick hunt around using Google to see if this has been a problem for anyone else, and found this: A ChildWindow management service for MVVM applications, which is great if you are using PRISM/Unity. As I say, I found that after I was done, and my service is very different, and does not rely on any IOC or PRISM for that matter, but if you use that combo, that article is quite interesting. Check it out.
Threading Helpers
I have included several threading helpers.
Dispatcher Extension Methods
These simple Dispatcher
extension methods provide convenient syntax when working with the Dispatcher
:
public static class DispatcherExtensions
{
#if !SILVERLIGHT
public static void InvokeIfRequired(this Dispatcher dispatcher,
Action action, DispatcherPriority priority)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(priority, action);
}
else
{
action();
}
}
#endif
public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
{
if (!dispatcher.CheckAccess())
{
#if SILVERLIGHT
dispatcher.BeginInvoke(action);
#else
dispatcher.Invoke(DispatcherPriority.Normal, action);
#endif
}
else
{
action();
}
}
public static void InvokeInBackgroundIfRequired(this Dispatcher dispatcher,
Action action)
{
if (!dispatcher.CheckAccess())
{
#if SILVERLIGHT
dispatcher.BeginInvoke(action);
#else
dispatcher.Invoke(DispatcherPriority.Background, action);
#endif
}
else
{
action();
}
}
public static void InvokeAsynchronouslyInBackground(this Dispatcher dispatcher,
Action action)
{
#if SILVERLIGHT
dispatcher.BeginInvoke(action);
#else
dispatcher.BeginInvoke(DispatcherPriority.Background, action);
#endif
}
}
Which allows you to do something like this:
uiDispatcher.InvokeIfRequired(()=>
{
});
Dispatcher SynchronizationContext
When you use Windows Forms, there is a SynchronizationContext
which may be used to send and post delegates on to. Which I discuss in this threading article. It is possible to create one for WPF, which is exactly what my good friend Daniel Vaughan did once, and I have included that code within this mini SL MVVM library. Basically, it boils down to this one UISynchronizationContext
class. Cheers Daniel.
public class UISynchronizationContext : ISynchronizationContext
{
#region Data
private DispatcherSynchronizationContext context;
private Dispatcher dispatcher;
private readonly object initializationLock = new object();
#endregion
#region Singleton implementation
static readonly UISynchronizationContext instance = new UISynchronizationContext();
public static ISynchronizationContext Instance
{
get
{
return instance;
}
}
#endregion
#region Public Methods
public void Initialize()
{
EnsureInitialized();
}
#endregion
#region ISynchronizationContext Members
public void Initialize(Dispatcher dispatcher)
{
ArgumentHelper.AssertNotNull(dispatcher, "dispatcher");
lock (initializationLock)
{
this.dispatcher = dispatcher;
context = new DispatcherSynchronizationContext(dispatcher);
}
}
public void InvokeAsynchronously(SendOrPostCallback callback, object state)
{
ArgumentHelper.AssertNotNull(callback, "callback");
EnsureInitialized();
context.Post(callback, state);
}
public void InvokeAsynchronously(Action action)
{
ArgumentHelper.AssertNotNull(action, "action");
EnsureInitialized();
if (dispatcher.CheckAccess())
{
action();
}
else
{
dispatcher.BeginInvoke(action);
}
}
public void InvokeSynchronously(SendOrPostCallback callback, object state)
{
ArgumentHelper.AssertNotNull(callback, "callback");
EnsureInitialized();
context.Send(callback, state);
}
public void InvokeSynchronously(Action action)
{
ArgumentHelper.AssertNotNull(action, "action");
EnsureInitialized();
if (dispatcher.CheckAccess())
{
action();
}
else
{
context.Send(delegate { action(); }, null);
}
}
public bool InvokeRequired
{
get
{
EnsureInitialized();
return !dispatcher.CheckAccess();
}
}
#endregion
#region Private Methods
private void EnsureInitialized()
{
if (dispatcher != null && context != null)
{
return;
}
lock (initializationLock)
{
if (dispatcher != null && context != null)
{
return;
}
try
{
dispatcher = System.Windows.Deployment.Current.Dispatcher;
context = new DispatcherSynchronizationContext(dispatcher);
}
catch (InvalidOperationException)
{
throw new Exception("Initialised called from non-UI thread.");
}
}
}
#endregion
}
Which we can then use to do all our Dispatcher
related thread marshalling with, such as this:
synContext.InvokeSynchronously(delegate()
{
FileMessage = "Total file size: " + FileSize + " Uploading: " +
string.Format("{0:###.00}%", (double)dataSent / (double)dataLength * 100);
});
Where the FileMessage
property is being bound in the XAML, and the FileMessage
property exists on the ViewModel, and this is inside a threaded context, so we must ensure that the FileMessage
property is updated on the UI's Dispatcher
thread.
What it Doesn't Do
There is no support for any of the following:
IEditableObject
support for ViewModels
IDataErrorInfo
/ Validation / Business Rules
- DataWrappers, as found in my Cinch WPF MVVM Framework
- Running a
ICommand
in a ViewModel in response to a UI event such as MouseDown. This looks to be standard stuff in Silverlight using Microsoft.Windows.Interactivity.dll, and then making use of the TargetedTriggerAction<T>
. I discuss this on my blog http://sachabarber.net/?p=510 (albeit that it is for WPF, which allows us to inherit from ICommandSource
, where as Silverlight does not). I am sure someone can make it work just fine, and there is probably someone out there that has already done this.
All of which are covered by my Cinch WPF MVVM Framework, but this app took like two days to write, and my Cinch WPF MVVM Framework took ages to write. So one has to make allowances.
These could be added easily enough, now that the IEditableObject/IDataErrorInfo
are part of the Silverlight SDK (IDataErrorInfo
was missing in the past).
Right, so now that I have explained the small Silverlight MVVM framework that is included in this code demo app.
Special Thanks / Credits
I should point out that this code makes use of some code that I found over at a one John Mendezes' blog: File Upload in Silverlight 3, which John states he grabbed from an Open Source CodePlex project: Silverlight Multi File Uploader v3.0.
All I wanted to do was mess around with Silverlight 4 and also try and do it in a good MVVM way, but I also needed to upload images as part of something bigger I am planning, so I started with John Mendezes' code.
Back to the Program
I suppose we need to steer back to the actual demo app, which if you recall, allows a user to upload a file from their local file system to a remote web server, and if the file is an image, it will be shown in a ChildWindow
.
Well, believe it or not, now that we have covered the small Silverlight MVVM framework attached, it really boils down to a very simple object model.
We have a View (FileUpload
) that makes use of a ViewModel (ImageUploadViewModel
). John's original code was all code-behind, so the original exercise I set myself was to MVVM it.
Let's have a look at the MVVM-ed code:
Doing the File Upload
As stated, there is a ImageUploadViewModel
which has a ICommand
exposed which the Silverlight View FileUpload
uses, as follows:
<Button Content="Upload File" Command="{Binding UploadCommand}" />
And when the user clicks this button, the following logic is run in the ImageUploadViewModel
; note the use of the ISLOpenFileService
service to show an open file dialog.
public ImageUploadViewModel()
{
ofd = GetService<ISLOpenFileService>();
}
private void ExecuteUploadCommand()
{
ofd.Multiselect = false;
if (ofd.ShowDialog() == true)
{
if (ofd.File != null)
fileToUpload = ofd.File;
IsBusy = true;
this.UploadFile();
}
}
When the user picks a file, the process of uploading the file happens, which calls the UploadFile()
method which looks like this:
private void UploadFile()
{
FileSize = this.GetFileSize(fileToUpload.Length);
this.StartUpload(fileToUpload);
}
Which in turn calls the StartUpload()
method, which looks like this, where this will be called numerous times to upload the file in chunks:
private void StartUpload(FileInfo file)
{
uploadedFile = file;
fileStream = uploadedFile.OpenRead();
dataLength = fileStream.Length;
long dataToSend = dataLength - dataSent;
bool isLastChunk = dataToSend <= ChunkSize;
bool isFirstChunk = dataSent == 0;
docType = imageFileExtensions.Contains(
uploadedFile.Extension.ToLower()) ?
documentType.Image : documentType.Document;
UriBuilder httpHandlerUrlBuilder =
new UriBuilder(string.Format("{0}/FileUpload.ashx",baseUri));
httpHandlerUrlBuilder.Query =
string.Format("{5}file={0}&offset={1}&last={2}&first={3}&docType={4}",
uploadedFile.Name, dataSent, isLastChunk, isFirstChunk, docType,
string.IsNullOrEmpty(httpHandlerUrlBuilder.Query) ? "" :
httpHandlerUrlBuilder.Query.Remove(0, 1) + "&");
HttpWebRequest webRequest =
(HttpWebRequest)WebRequest.Create(httpHandlerUrlBuilder.Uri);
webRequest.Method = "POST";
webRequest.BeginGetRequestStream(
new AsyncCallback(WriteToStreamCallback), webRequest);
}
Where this upload is being directed to a standard HttpHandler (FileUpload.ashx) in the Silverlight hosting web application (TestSL4.Web) that actually saves the file to the remote webserver's virtual directory. This call to the HttpHandler (FileUpload.ashx) is made using Asynchronous Delegates (BeginInvoke/EndInvoke), so there is a AsyncCallback
delegate provided to point to the WriteToStreamCallback()
method.
Here is that HttpHandler code:
<%@ WebHandler Language="C#" Class="FileUpload" %>
using System;
using System.Web;
using System.IO;
using System.Web.Hosting;
using System.Diagnostics;
/// <summary>
/// Saves the posted content as a file to the local web file system. The request
/// is expected to come from the <see cref="TestSL4.ImageUploadViewModel">
/// ImageUploadViewModel</see>
/// ViewModel
/// </summary>
public class FileUpload : IHttpHandler {
private HttpContext _httpContext;
private string _tempExtension = "_temp";
private string _fileName;
private string _docType;
private bool _lastChunk;
private bool _firstChunk;
private long _startByte;
private StreamWriter _debugFileStreamWriter;
private TextWriterTraceListener _debugListener;
public void ProcessRequest(HttpContext context)
{
_httpContext = context;
if (context.Request.InputStream.Length == 0)
throw new ArgumentException("No file input");
try
{
GetQueryStringParameters();
string uploadFolder = GetUploadFolder();
string tempFileName = _fileName + _tempExtension;
if (_firstChunk)
{
if (!Directory.Exists(
@HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder))
Directory.CreateDirectory(
@HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder);
//Delete temp file
if (File.Exists(@HostingEnvironment.ApplicationPhysicalPath +
"/" + uploadFolder + "/" + tempFileName))
File.Delete(@HostingEnvironment.ApplicationPhysicalPath +
"/" + uploadFolder + "/" + tempFileName);
//Delete target file
if (File.Exists(@HostingEnvironment.ApplicationPhysicalPath +
"/" + uploadFolder + "/" + _fileName))
File.Delete(@HostingEnvironment.ApplicationPhysicalPath +
"/" + uploadFolder + "/" + _fileName);
}
using (FileStream fs = File.Open(@HostingEnvironment.ApplicationPhysicalPath +
"/" + uploadFolder + "/" + tempFileName, FileMode.Append))
{
SaveFile(context.Request.InputStream, fs);
fs.Close();
}
if (_lastChunk)
{
File.Move(HostingEnvironment.ApplicationPhysicalPath + "/" + uploadFolder +
"/" + tempFileName, HostingEnvironment.ApplicationPhysicalPath + "/" +
uploadFolder + "/" + _fileName);
}
}
catch (Exception e)
{
throw;
}
}
private void GetQueryStringParameters()
{
_fileName = _httpContext.Request.QueryString["file"];
_docType = _httpContext.Request.QueryString["docType"];
_lastChunk = string.IsNullOrEmpty(_httpContext.Request.QueryString["last"])
? true : bool.Parse(_httpContext.Request.QueryString["last"]);
_firstChunk = string.IsNullOrEmpty(_httpContext.Request.QueryString["first"])
? true : bool.Parse(_httpContext.Request.QueryString["first"]);
_startByte = string.IsNullOrEmpty(_httpContext.Request.QueryString["offset"])
? 0 : long.Parse(_httpContext.Request.QueryString["offset"]); ;
}
private void SaveFile(Stream stream, FileStream fs)
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
{
fs.Write(buffer, 0, bytesRead);
}
}
protected string GetUploadFolder()
{
string folder = "";
switch (_docType)
{
case "Document":
folder = "documents/uploads";
break;
case "Image":
folder = "documents/images";
break;
default:
folder = "documents";
break;
}
if (string.IsNullOrEmpty(folder))
folder = "documents";
return folder;
}
public bool IsReusable {
get {
return false;
}
}
}
You can also notice that there is a AsyncCallback
delegate provided to point to the WriteToStreamCallback()
method.
private void WriteToStreamCallback(IAsyncResult asynchronousResult)
{
HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState;
Stream requestStream = webRequest.EndGetRequestStream(asynchronousResult);
byte[] buffer = new Byte[4096];
int bytesRead = 0;
int tempTotal = 0;
fileStream.Position = dataSent;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length))
!= 0 && tempTotal + bytesRead < ChunkSize)
{
requestStream.Write(buffer, 0, bytesRead);
requestStream.Flush();
dataSent += bytesRead;
tempTotal += bytesRead;
UpdateShowProgress(false);
}
requestStream.Close();
webRequest.BeginGetResponse(
new AsyncCallback(ReadHttpResponseCallback), webRequest);
}
Showing the ChildWindow When the File has Completely Been Uploaded
As we want to know whether the file upload succeeded or not, the HttpHandler's response is also used within a AsyncCallback
delegate provided to point to the ReadHttpResponseCallback()
method. It is within the ReadHttpResponseCallback()
method that the UI status is updated by calling the UpdateShowProgress()
method, and the results of the HttpHandler (FileUpload.ashx) stream are examined, and if the entire original file bytes have not yet been sent, a new call to StartUpload()
will be made to upload the next chunk.
private void ReadHttpResponseCallback(IAsyncResult asynchronousResult)
{
try
{
HttpWebRequest webRequest =
(HttpWebRequest)asynchronousResult.AsyncState;
HttpWebResponse webResponse =
(HttpWebResponse)webRequest.EndGetResponse(asynchronousResult);
StreamReader reader = new StreamReader(webResponse.GetResponseStream());
string responsestring = reader.ReadToEnd();
reader.Close();
}
catch
{
synContext.InvokeSynchronously(delegate()
{
ShowError();
});
}
if (dataSent < dataLength)
{
StartUpload(uploadedFile);
UpdateShowProgress(false);
}
else
{
fileStream.Close();
fileStream.Dispose();
UpdateShowProgress(true);
}
}
If the full file contents have been uploaded, the file is shown in a ChildWindow
as shown below:
private void UpdateShowProgress(bool complete)
{
if (complete)
{
synContext.InvokeSynchronously(delegate()
{
FileMessage = "complete";
});
if (docType == documentType.Image)
{
AutoResetEvent are = new AutoResetEvent(false);
synContext.InvokeSynchronously(delegate()
{
FinalFilePath = String.Format(@"{0}/{1}/{2}",
baseUri, GetUploadFolder(), uploadedFile.Name);
uploadedFile = null;
IsBusy = false;
popupVisualizer.ShowModally("ImagePopup", this, are, (s, e) =>
{
if (!e.Result.Value)
{
msgbox.ShowInformation(
"You clicked Cancel. So this could be used " +
"to delete the File from the WebServer.\r\n" +
"Possibly on a new thread, " +
"Instead of showing this blocking Modal MessageBox");
}
});
});
are.WaitOne();
}
else
{
synContext.InvokeSynchronously(delegate()
{
uploadedFile = null;
IsBusy = false;
});
}
fileToUpload = null;
dataSent = 0;
dataLength = 0;
fileStream = null;
FileSize = "";
}
else
{
synContext.InvokeSynchronously(delegate()
{
FileMessage = "Total file size: " + FileSize +
" Uploading: " +
string.Format("{0:###.00}%",
(double)dataSent / (double)dataLength * 100);
});
}
}
As always, votes / comments are welcome.