Le problème
Habituellement, lorsque l'on utilise Prism, l'intégration avec le framework de navigation Silverlight n'est pas tip-top... La ruse consiste bien souvent à avoir des pages qui sont du pur XAML (donc pas de ViewModel) et qui contiennent des régions qui sont des Views (et qui elles ont bien des ViewModels) instanciées par Unity.
Si on passe un coup de reflector dans l'assembly de navigation, on trouve vite le coupable! La classe est PageResourceContentLoader, c'est elle qui fait la glue entre une uri et une instance. Dans la méthode BeginLoad_OnUIThread, on s'apercoit de ca:
result.Content = Activator.CreateInstance(typeFromAnyLoadedAssembly);
L'idée est donc d'éviter d'utiliser la classe Activator mais de passer par notre container.
La solution
Il existe une interface INavigationContentLoader qu'il faut implémenter pour pouvoir créer un ContentLoader personalisé.
public interface INavigationContentLoader
{
IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState);
void CancelLoad(IAsyncResult asyncResult);
bool CanLoad(Uri targetUri, Uri currentUri);
LoadResult EndLoad(IAsyncResult asyncResult);
}
On remarque l'utilisation d'un pattern async (BeginLoad & EndLoad).
Nous allons donc implémenter notre propre ContentLoader en s'inspirant largement de ce qui existe déjà dans PageResourceContentLoader:
- Il nous faut une fonction qui à partir d'un type sous forme littérale nous retourne un objet Type. Deux méthodes s'offrent à nous: Passer par la méthode statique GetType de Type ou naviguer dans les Parts de l'application. Etant donné que GetType nécessite le nom complet qualifié du type. J'ai opté pour la seconde manière.
- BeginLoad crée un élement implémentant IAsyncResult, nécessaire au pattern async. La création effective se fait sur le thread de l'UI.
- CancelLoad reste vide (on ne s'en sert pas).
- CanLoad vérifie que l'uri de destination est navigable, ici on vérifie que le type est connu.
- EndLoad utilise l'élement implémentant IAsyncResult pour retourner l'instance.
Voici une implémentation possible:
public class UnityContentLoader : INavigationContentLoader
{
private Type ResolveType(String typeName)
{
Type type = null;
foreach (AssemblyPart part in Deployment.Current.Parts)
{
StreamResourceInfo resourceStream = Application.GetResourceStream(new Uri(part.Source, UriKind.Relative));
if (resourceStream != null)
{
Assembly assembly = new AssemblyPart().Load(resourceStream.Stream);
if (assembly != null)
{
type = Type.GetType(typeName + "," + assembly, false);
if (type != null)
return type;
}
}
}
return null;
}
private IUnityContainer unityContainer
{
get
{
return ServiceLocator.Current.GetInstance<IUnityContainer>();
}
}
#region INavigationContentLoader Members
public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
{
var asyncResult = new UnityContentLoaderAsyncResult(targetUri.OriginalString, asyncState);
if (SynchronizationContext.Current != null)
SynchronizationContext.Current.Post(obj => BeginLoadOnUiThread(userCallback, asyncResult), null);
else
Deployment.Current.Dispatcher.BeginInvoke(() => BeginLoadOnUiThread(userCallback, asyncResult));
return asyncResult;
}
public bool CanLoad(Uri targetUri, Uri currentUri)
{
return ResolveType(targetUri.OriginalString) != null;
}
public void CancelLoad(IAsyncResult asyncResult)
{
}
public LoadResult EndLoad(IAsyncResult asyncResult)
{
var result = asyncResult as UnityContentLoaderAsyncResult;
if (result.Exception != null)
throw result.Exception;
return new LoadResult(result.Content);
}
#endregion
private void BeginLoadOnUiThread(AsyncCallback userCallback, UnityContentLoaderAsyncResult asyncResult)
{
if (asyncResult.Exception != null)
{
asyncResult.IsCompleted = true;
if (userCallback != null)
userCallback(asyncResult);
return;
}
try
{
var type = ResolveType(asyncResult.TypeName);
if (type == null)
throw new ArgumentException("Unable to find type requested");
asyncResult.Content = unityContainer.Resolve(type);
}
catch (Exception e)
{
asyncResult.Exception = e;
}
finally
{
asyncResult.IsCompleted = true;
if (userCallback != null)
userCallback(asyncResult);
}
}
private class UnityContentLoaderAsyncResult : IAsyncResult
{
private readonly String _typeName;
private readonly object _asyncState;
internal UnityContentLoaderAsyncResult(String typeName, object asyncState)
{
_typeName = typeName;
_asyncState = asyncState;
}
#region IAsyncResult Members
public object AsyncState { get { return _asyncState; } }
public WaitHandle AsyncWaitHandle { get { return null; } }
public bool CompletedSynchronously { get { return false; } }
public bool IsCompleted { get; internal set; }
#endregion
internal String TypeName { get { return _typeName; } }
internal Exception Exception { get; set; }
internal object Content { get; set; }
}
}
Utilisation
L'utilisation n'est pas très complexe. Il suffit de déclarer une instance comme ressource dans l'application et d'adapter son UriMapper:
<Application x:Class="ViewFirstDemo.Core.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:core="http://www.viewfirstdemo.net/core">
<Application.Resources>
<sdk:UriMapper x:Key="UriMapper">
<sdk:UriMapping Uri="" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.Home.HomeView" />
<sdk:UriMapping Uri="/Home" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.Home.HomeView" />
<sdk:UriMapping Uri="/Dummy" MappedUri="ViewFirstDemo.Modules.DummyModule.Pages.Dummy" />
<sdk:UriMapping Uri="/About" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.About.AboutView" />
</sdk:UriMapper>
<core:UnityContentLoader x:Key="ContentLoader" />
</Application.Resources>
</Application>
L'utilisation dans la frame de navigation utilise ces deux ressources:
<sdk:Frame UriMapper="{StaticResource UriMapper}" ContentLoader="{StaticResource ContentLoader}"/>
Vos pages peuvent désormais avoir un ViewModel!