Silverlight Navigation Framework, Prism, Unity et téléchargement à la volée (On-Demand) des XAP

February 15, 2011 at 9:46 PMNathanael

Pour continuer sur la lancée du post précédent, je me suis dit qu'il fallait que j'ajoute le téléchargement à la volée des xaps. Ainsi on peut découper son application et le client ne télécharge que ce qui lui est nécessaire!
Pour cela, il "suffit" de trafiquer le UnityContentLoader du post précédent pour qu'à la demande d'une page, si le type n'est pas encore chargé, il y'ait chargement. De plus, comme il y'a un pattern async, c'est plutôt simple à mettre en place: tout se fait dans le BeginLoad (et aussi un peu dans le CanLoad en fait).

public class UnityContentLoader : INavigationContentLoader
{
	private static Type ResolveType(String typeName)
	{
		var containerRegistration = UnityContainer.Registrations.FirstOrDefault(cr => cr.RegisteredType.FullName == typeName);
			if (containerRegistration == null)
			return null;
			return containerRegistration.RegisteredType;
	}

	private static IUnityContainer UnityContainer
	{
		get
		{
			return ServiceLocator.Current.GetInstance<IUnityContainer>();
		}
	}

	private static IModuleCatalog ModuleCatalog
	{
		get
		{
			return ServiceLocator.Current.GetInstance<IModuleCatalog>();
		}
	}

	private static IModuleManager ModuleManager
	{
		get
		{
			return ServiceLocator.Current.GetInstance<IModuleManager>();
		}
	}

	#region INavigationContentLoader Members

	public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
	{
		var splittedName = targetUri.OriginalString.Split(',').Select(s => s.Trim()).ToArray();

		var asyncResult = new UnityContentLoaderAsyncResult(splittedName[0], asyncState);

		Action<AsyncCallback, UnityContentLoaderAsyncResult> action = (callback, result) =>
			{
				if (SynchronizationContext.Current != null)
					SynchronizationContext.Current.Post(obj => BeginLoadOnUiThread(callback, result), null);
				else
					Deployment.Current.Dispatcher.BeginInvoke(() => BeginLoadOnUiThread(callback, result));
			};

		//Module information given, checking whether we need to load the module or not
		if (splittedName.Count() > 1)
		{
			var module = ModuleCatalog.Modules.First(m => m.ModuleName == splittedName[1]);

			if (module == null)
				asyncResult.Exception = new ModuleNotFoundException();
			else
			{
				if (module.State == ModuleState.NotStarted)
				{
					ModuleManager.LoadModuleCompleted += (sender, args) =>
																				{
																					if (args.ModuleInfo.ModuleName == module.ModuleName)
																					{
																						action(userCallback, asyncResult);
																					}
																				};
					ModuleManager.LoadModule(module.ModuleName);
					return asyncResult;
				}
			}

			action(userCallback, asyncResult);
			return asyncResult;
		}
		else
		{
			//No module information given, assuming assemblies are already loaded
			action(userCallback, asyncResult);
			return asyncResult;
		}
	}

	public bool CanLoad(Uri targetUri, Uri currentUri)
	{
		var splittedName = targetUri.OriginalString.Split(',').Select(s => s.Trim()).ToArray();
		if (splittedName.Count() > 1)
		{
			var module = ModuleCatalog.Modules.First(m => m.ModuleName == splittedName[1]);

			if (module == null)
				return false;

			if (module.State == ModuleState.NotStarted)
				return true;
		}

		return ResolveType(splittedName[0]) != 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 static 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; }
	}
}

Tout se joue dans l'UriMapper : lorsqu'une uri contient uniquement un type, on présume que le module est chargé en même temps que l'appli. Lorsque dans l'uri il y'a le type ainsi que le nom du module alors on présume que le module n'est pas forcément chargé et on s'en assure auparavant. On reprend la marche normale une fois chargé.

Voici un exemple d'UriMapper qui va bien:

<sdk:UriMapper x:Key="UriMapper">
	<sdk:UriMapping Uri="" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.Home.IHomeView" />
	<sdk:UriMapping Uri="/Home" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.Home.IHomeView" />
	<sdk:UriMapping Uri="/Dummy" MappedUri="ViewFirstDemo.Modules.DummyModule.Pages.Dummy, DummyModule" />
	<sdk:UriMapping Uri="/About" MappedUri="ViewFirstDemo.Modules.ShellModule.Pages.About.AboutView" />
</sdk:UriMapper>

(On remarque bien sur /Dummy l'ajout à la fin du nom du module séparé par une virgule)

Avec le ModuleCatalog correspondant:

<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
						  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
						  xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
	<Modularity:ModuleInfo Ref="Shell.xap"
						   InitializationMode="WhenAvailable"
						   ModuleName="ShellModule"
						   ModuleType="ViewFirstDemo.Modules.ShellModule.ShellModule, ViewFirstDemo.Modules.ShellModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
	<Modularity:ModuleInfo Ref="Dummy.xap"
						   InitializationMode="OnDemand"
						   ModuleName="DummyModule"
						   ModuleType="ViewFirstDemo.Modules.DummyModule.DummyModule, ViewFirstDemo.Modules.DummyModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleCatalog>

     

Navigation, Prism, Unity et ViewModel des pages

February 15, 2011 at 2:24 PMNathanael

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!