Silverlight Navigation Framework and Prism (Custom Content Loader for Navigation Framework)
As I was working on the administration tool I mentioned previously I needed to add a navigation feature for my solution. I had 2 modules (at the moment) and I needed to load them at separate pages on the Shell where I would be able to use a URL to access each different page.
Before you go on reading, allow me to note that I believe there are many other (and even more elite ways) of achieving this. For me the easiest was to do what I am going to explain below. I also want to underline that I am a generalist and I am one those people who want to get things done as soon as possible but also as neatly as possible.
The first thing I did was to create multiple pages and integrate the Navigation Application template to my solution. So far everything was great. Next thing I did was to add regions to each navigation page with different names. I modified the bootstrapper to load my modules on-demand so that until the user would view a page they would not be loaded.
All was great except the fact that nothing was displayed in my pages – which makes sense since they were to be loaded on-demand.
I added a new class called ModuleMapper – a static one to include a static dictionary that would let me map my page URIs with my Module Type names. This way, I would be able to know which module to load when I navigated to a page. The code for ModuleMapper is below and as you can see there is nothing fancy.
1: public static class ModuleMapper
2: {
3: /// <summary>
4: /// Gets or sets the module maps.
5: /// </summary>
6: /// <value>The module maps.</value>
7: public static Dictionary<string, string> ModuleMaps { get; set; }
8:
9: static ModuleMapper()
10: {
11: ModuleMaps = new Dictionary<string, string>();
12: }
13: }
I then modified my shell to get references for module manager, module catalog and unity container.
1: public NavShell(IModuleManager moduleManager, IModuleCatalog moduleCatalog, IUnityContainer container)
2: {
3: InitializeComponent();
4: this.ModuleManager = moduleManager;
5: this.ModuleCatalog = moduleCatalog;
6: this.Container = container;
7: }
Next thing I did was to add a function to my shell’s code behind that would let me load the module that corresponds to the page I am navigating to.
1: private void LoadModule(string uri)
2: {
3: // if link requires a module then load it
4: if (ModuleMapper.ModuleMaps.ContainsKey(uri))
5: {
6: var module = ModuleCatalog.Modules.SingleOrDefault(x => x.ModuleName == ModuleMapper.ModuleMaps[uri]);
7:
8: if (module == null)
9: {
10: throw new InvalidOperationException("Requested module is not loaded");
11: }
12:
13: if (module.State == ModuleState.NotStarted)
14: {
15: ModuleManager.LoadModule(ModuleMapper.ModuleMaps[uri]);
16: }
17: }
18: }
Last but not least, I added an event handler for my frame’s navigated event that would let me load the relevant module as I am navigating.
1: private void ContentFrame_Navigated(object sender, NavigationEventArgs e)
2: {
3: LoadModule(e.Uri.ToString());
4:
5: foreach (UIElement child in LinksStackPanel.Children)
6: {
7: HyperlinkButton hb = child as HyperlinkButton;
8: if (hb != null && hb.NavigateUri != null)
9: {
10: if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))
11: {
12: VisualStateManager.GoToState(hb, "ActiveLink", true);
13: }
14: else
15: {
16: VisualStateManager.GoToState(hb, "InactiveLink", true);
17: }
18: }
19: }
20: }
What was left for me to do was to start using my ModuleMapper. The hook point I chose was in my bootstrapper. Inside my bootstrapper I had a function called GetModuleCatalog that looked perfect to inject my ModuleMapper code into.
1: protected override IModuleCatalog GetModuleCatalog()
2: {
3: ModuleCatalog catalog = new ModuleCatalog();
4: catalog.AddModule(typeof(Modules.Module1), InitializationMode.OnDemand);
5: catalog.AddModule(typeof(Modules.Module2), InitializationMode.OnDemand);
6:
7: ModuleMapper.ModuleMaps.Add("/ModuleViewPage1",typeof(Modules.Modile1).Name);
8: ModuleMapper.ModuleMaps.Add("/ModuleViewPage2", typeof(Modules.Module2).Name);
9:
10: return catalog;
11: }
Once I did all this, I was excited to click on the “Run” to see what happens. And guess what – all worked fine. At least for a while. I navigated to the first page and the module loaded fine. I navigated to the second page and the module loaded fine. I navigated back to the first page and my application crashed.
The exception I got when my application crashed was: “Unhandled Error in Silverlight Application Code:4004, System.InvalidOperationException: Element is already the child of another element”.
After some debugging and doing a small research I realized that the problem was related with the Navigation Framework. Each time navigation framework displays a page, it creates a new instance of the class associated with the page. This makes absolute sense – makes absolute sense if you are destruct your modules and load them again when necessary.
Here is what is causing this problem. On your first call to the page, RegionManager makes sure that your view is instantiated and associated with that view. On subsequent calls, the region used previously is destroyed and your region manager wants to associate the old view to a new region created when your page class is instantiated. This causes the problem above since your view was already set as a child of another element.
The solution that I came up with was to implement my own version of ContentLoader for Navigation Framework where each content would behave as a Singleton. So here is what I did (some of the code is based on other blog posts Content Loaders).
First I created my Async Result class that would implement IAsyncResult interface and would act as my result object.
1: public class SingletonContentLoaderAsyncResult : IAsyncResult
2: {
3: public object Result { get; set; }
4:
5: public SingletonContentLoaderAsyncResult(object asyncState)
6: {
7: this.AsyncState = asyncState;
8: this.AsyncWaitHandle = new ManualResetEvent(true);
9: }
10: #region IAsyncResult Members
11:
12: public object AsyncState
13: {
14: get;
15: private set;
16: }
17:
18: public System.Threading.WaitHandle AsyncWaitHandle
19: {
20: get;
21: private set;
22: }
23:
24: public bool CompletedSynchronously
25: {
26: get { return true; }
27: }
28:
29: public bool IsCompleted
30: {
31: get { return true; }
32: }
33:
34: #endregion
35: }
Next I created my Singleton Content Loader class. It would make sure that it would keep references to the created page contents and serve them back on subsequent requests. If the page was not loaded before, it would create an instance and store the page in its local static list.
1: public class SingletonContentLoader : INavigationContentLoader
2: {
3: static SingletonContentLoader()
4: {
5: InstantiatedUserControls = new Dictionary<string, object>();
6: }
7:
8: private string GetTypeNameFromUri(Uri uri)
9: {
10: if (!uri.IsAbsoluteUri)
11: uri = new Uri(new Uri("dummy:///", UriKind.Absolute), uri.OriginalString);
12: return Uri.UnescapeDataString(uri.AbsolutePath.Substring(1));
13: }
14:
15: #region INavigationContentLoader Members
16:
17: public bool CanLoad(Uri targetUri, Uri currentUri)
18: {
19: string typeName = GetTypeNameFromUri(targetUri);
20: Type t = Type.GetType(typeName, false, true);
21: if (t == null)
22: return false;
23: var defaultConstructor = t.GetConstructor(new Type[0]);
24: if (defaultConstructor == null)
25: return false;
26: return true;
27: }
28:
29: public static Dictionary<string, object> InstantiatedUserControls { get; set; }
30:
31: public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
32: {
33: var result = new SingletonContentLoaderAsyncResult(asyncState);
34: Type t = Type.GetType(GetTypeNameFromUri(targetUri), false, true);
35: object instance = null;
36: if (t != null)
37: {
38: if (InstantiatedUserControls.ContainsKey(t.Name))
39: {
40: instance = InstantiatedUserControls[t.Name];
41: }
42: else
43: {
44: instance = Activator.CreateInstance(t);
45: InstantiatedUserControls[t.Name] = instance;
46: }
47: }
48: result.Result = instance;
49: userCallback(result);
50: return result;
51: }
52:
53: public LoadResult EndLoad(IAsyncResult asyncResult)
54: {
55: return new LoadResult(((SingletonContentLoaderAsyncResult)asyncResult).Result);
56: }
57:
58: public void CancelLoad(IAsyncResult asyncResult)
59: {
60: return;
61: }
62:
63: #endregion
64: }
Last thing to do was to add this content loader to my frame and modify my UriMapper to map each URI to a full class name which is to be loaded. Below is the example code from my navShell.xaml.
1: <navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}" Navigating="ContentFrame_Navigating"
2: Source="/Home" Navigated="ContentFrame_Navigated" NavigationFailed="ContentFrame_NavigationFailed">
3: <navigation:Frame.ContentLoader>
4: <local:SingletonContentLoader />
5: </navigation:Frame.ContentLoader>
6: <navigation:Frame.UriMapper>
7: <uriMapper:UriMapper>
8: <uriMapper:UriMapping Uri="" MappedUri="AdministrationTools.UI.Views.Home"/>
9: <uriMapper:UriMapping Uri="/{pageName}" MappedUri="AdministrationTools.UI.Views.{pageName}"/>
10: </uriMapper:UriMapper>
11: </navigation:Frame.UriMapper>
12: </navigation:Frame>
All this code put together does the magic – with the code above you have your own content loader that instantiates the objects once and serves them back on subsequent calls.
ps: I just finished writing this post on a Friday evening and I am planning to head back home now – so no grammar or typo checking.
Comments
- Anonymous
August 26, 2010
this is just great, however I do have a question : How should one handle a scenario when the user is supplying an url trailed with parameters? like mypage/Home?param=5 ?