region & frame navigation with Modules OnDemand

Topics: Prism v4 - Silverlight 4
Jan 13, 2012 at 7:27 PM

I have followed through with Karl Shifflett's example and read all his replies on it. He doesn't have any examples of accessing modules loaded OnDemand. I think this is a great solution for navigation, especially for my team whom has used the silverlight navigation API but not the region API.

What I need todo though is load in modules on demand, and as Karl has mentioned in his replies I should ask here for help.

I'm working in Silverlight 4 with Prism 4 with MEF (SL5 on the release of Prism 4.1), all my modules are OnDemand and discovered through the ModuleCatalog.

Ok so originally I was going to ask a question, but while typing out the question I realized where I was making a mistake in my MappingUri, so instead I'll share my solution for loading OnDemand that I've got currently, I'd love some feed back and possibly even a better way todo this. My current solution has some TODOs, one of which is display the load progress of a module and recall the navigate.

So here is my Navigation:Frame within my ShellView.xaml  (my Project's name is Shell btw)

 

<navigation:Frame
                x:Name="ContentFrame"
                Source=""
                Navigated="ContentFrame_Navigated"
                NavigationFailed="ContentFrame_NavigationFailed"
                prism:RegionManager.RegionName="MainContentRegion"
                >
                    <navigation:Frame.ContentLoader>
                        <prism_Regions:FrameContentLoader RegionName="MainContentRegion"/>
                    </navigation:Frame.ContentLoader>

                    <navigation:Frame.UriMapper>
                        <uriMapper:UriMapper>
                            
                            <!--Root View-->
                            <uriMapper:UriMapping Uri="" MappedUri="/Shell.Views.TestView"/>

                            <!--Used to navigate to any Module's View-->
                            <uriMapper:UriMapping Uri="/{moduleName}/{pageName}" MappedUri="{}{moduleName}.Views.{pageName}"/>

                            <!--Used to navigate to a page in the Shell-->
                            <uriMapper:UriMapping Uri="/{pageName}" MappedUri="Shell.Views.{pageName}"/>

                        </uriMapper:UriMapper>
                    </navigation:Frame.UriMapper>
                </navigation:Frame>

 

So I'm headed to a TestView on my Shell to begin which simply has a HyperlinkButton with it's NavigateUri="/AdminAccess/View1"
This is because currently I have to click the button twice, once to load and once to navigate.

The gotcha that really took me awhile was the UriMapping, one I had this guy at the bottom so /{pageName} was being called instead of the one I wanted, the next issue was I had for the MappedUri. MappedUri="/{moduleName}.Views.{pageName}" Now the issue with this is it'd set the Uri to a relative and do a Shell.{moduleName}.Views.{pageName} instead of {moduleName}.Views.{pageName}, but I couldn't put a "{moduleName} in because of the special use of {, but then I remembered you can just put {} and have it be happy.

<uriMapper:UriMapping Uri="/{moduleName}/{pageName}" MappedUri="{}{moduleName}.Views.{pageName}"/>

So now that I've got the right Uri being sent on click, I start figuring out how to load in the module. Its a little hacked together but works, thinking about it I should probably move it from a try{} catch{} to OnNavigationFailure() and do my logic there.

 

(Karl's code) Under FrameContentLoader.cs BeginLoad(...), we'll crash on trying  to LoadContent() into "var view" if the module hasn't been loaded yet. So I added a try{} catch{} to handle the exception and load in the module.

public System.IAsyncResult BeginLoad(System.Uri targetUri, System.Uri currentUri, System.AsyncCallback userCallback, Object asyncState) 
        {    
            EnsureRegionIsSet();

            object view = null;
            try
            {
                view = this._targetHandler.LoadContent(this._region, new NavigationContext(null, targetUri));
            }
            catch (InvalidOperationException e)
            {
                //TODO: Create a check to see if the URI is bad. If the URI looks correct, assume the module needs to be loaded


                //Check to see if Module has been loaded.
                var moduleCatalog = ServiceLocator.Current.GetInstance<IModuleCatalog>();
                string[] uriWords = targetUri.ToString().Split('.');
                string moduleType = uriWords[0]; //This should == {moduleName} from the UriMapping
                //TODO: Find a safer way to find the {moduleName} instead of assuming its array[0].
                foreach (ModuleInfo mi in moduleCatalog.Modules)
                {
                    string type = mi.ModuleType;
                    string[] typeWords = type.Split(',');

                    if (typeWords[1].Trim().CompareTo(moduleType) == 0)
                    {
                        //We found the Module, so grab the moduleManager and tell it to load it.
                        var moduleManager = ServiceLocator.Current.GetInstance<IModuleManager>();
                        moduleManager.LoadModule(typeWords[0]);

                        //TODO: Create a notification to tell the user we're loading their module, Maybe redirect to a loading page?
                        //TODO: Create a callback to recall Navigation on LoadComplete

                        break;
                    }
                }
                return null;
            }

            var effectiveView = GetFrameContent(view, this._region);

            var result = new LoadAsyncResult { FrameContent = effectiveView, ActualView = view, AsyncState = asyncState };

            if(userCallback != null) {
                userCallback(result);
            }

            return result;
        }

so this will have my Module load (its small so almost instant) and if I click the hyperlink button again it'll navigate my region frame to the view I want.

 

Any one have a better solution? Any suggestions for this one?

Developer
Jan 16, 2012 at 5:45 PM
Edited Jan 16, 2012 at 5:49 PM

Hi,

Thanks for sharing your findings with the rest of the community, as it might be useful for other users pursuing this scenario.

Also for those interested, I believe you might find these threads interesting, where related concerns are discussed:

Thanks,

Agustin Adami
http://blogs.southworks.net/aadami


Jan 17, 2012 at 5:10 PM

Thank you very much! gan_s solution in the 2nd link helped me, I was having issues with registering the callback event, forgot I could simply GetInstance for the IRegionManager and then .RequestNavigate().

Here's my solution for any one else for what I have right now. It runs based off exceptions for now, I could inline this with the navigation api and always check to see if the module has been loaded and I might have to if I can't figure out how to get the navigation request's target region. For now I actually don't mind the exception handling taking care of this for the simple fact that I didn't have to edit the libary code, just add business logic. This also makes it very easy to share.

This should go into your ShellView.xaml (or what ever you happen to call your root view) and within the <navigation:Frame> tag you'll need
NavigationFailed="ContentFrame_NavigationFailed"
to link up the event.

/// <summary>
        /// Handles the NavigationFailed event of the ContentFrame control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.Navigation.NavigationFailedEventArgs"/> instance containing the event data.</param>
        void ContentFrame_NavigationFailed(Object sender, System.Windows.Navigation.NavigationFailedEventArgs e)
        {
            e.Handled = true;
            
            Uri uri = e.Uri; //NOTE: This is the actual RequestNavigate's URI
            string[] uriWords = uri.ToString().Split('/');

            
            if (uriWords.Length >= 3) // assuming "", "{moduleName}", "{pageName}", ....
            {
                //Assume a module needs to be loaded
                //TODO: Find a safer way to find the {moduleName} instead of assuming its array[1].
                string moduleType = uriWords[1]; //This should == {moduleName} from the UriMapping.Uri
                
                //Attempt to find and load the {moduleName}.
                var moduleCatalog = ServiceLocator.Current.GetInstance<IModuleCatalog>();
                var notStartedModules = moduleCatalog.Modules.Where(mi => mi.State == ModuleState.NotStarted);
                foreach (ModuleInfo mi in notStartedModules)
                {
                    string type = mi.ModuleType;
                    string[] typeWords = type.Split(',');

                    if (typeWords[1].Trim().CompareTo(moduleType) == 0)
                    {
                        //We found the Module, so grab the PrismManagers and tell the Module to load.
                        var moduleManager = ServiceLocator.Current.GetInstance<IModuleManager>();
                        var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
                        
                        //TODO: Get a way to find the failed navigation's Region target
                        moduleManager.LoadModuleCompleted += (r, s) => regionManager.RequestNavigate(KnownRegions.MainContentRegion, uri);
                        moduleManager.LoadModule(typeWords[0]);

                        //MessageBox.Show("Page is loading");//TODO: Create a way to tell the user we're loading the module.
                        return;
                    }
                }                    
            }

            MessageBox.Show("Uri fail: " + e.Uri);
        }

Now when I try and navigate to a page on a module that is not loaded, it'll fail, load my module and then try and navigate again. For my case this works since we'll only have one RegionFrame (no nested ones), but I hate magic strings so I hope to fix the Region Target and figuring out how to grab the {moduleName}. Maybe if I dig into the actual Navigation API's code I can find a good place to let this sit. Any suggestions on how to get this done would be great!

Also note my solution is much like jandersen78's in the 2nd link in the above post. Only problem I have with his is I don't see that working for deep-linking the URL. (If I bookmark a page or type the URL in)

Jan 17, 2012 at 8:58 PM

Interesting error I'm getting, if I go to one of my deep links within a module (Module named AdminAccess, page named DashBoard)
So http://localhost:50932/#/AdminAccess/View1

If I hit the refresh button it'll work correctly half of the time; other times it'll pop up with "Uri fail: " [uri] msg. It'll be there loaded and everything, just seems like its calling the navigation to many times.

Developer
Jan 18, 2012 at 8:37 PM

Hi,

So far, we couldn’t find the cause of the error you mentioned, therefore it would be helpful if you could provide us with a repro sample application that portrays this scenario, so that we can help you further with this.

Thanks,

Agustin Adami
http://blogs.southworks.net/aadami


Jan 23, 2012 at 3:39 PM

Actually I think I might have figured it out. First a question, with the ModuleManager's LoadModuleCompleted event, that fires on ANY module loaded right? Because in debug mode I noticed what was happening was

Pseudo code
[Refresh]
Navigate to AdminAccess/View1
[Fail]
ModuleManager has 2 .xaps, my LoginModule.xap (State: LoadingTypes) & AdminAccess.xap (State: NotStarted)
[ExceptionCatch]
Start Loading AdminAccess
On Load Complete RequestNavigate()

[LoginModule Finishs loading]
Navigation Called
[Fail]
ModuleManager has 2 .xaps, my LoginModule.xap (State: Initialized) & AdminAccess.xap (State: LoadingTypes)
[ExceptionCatch]
My logic says "No modules to load, so must be bad URI" messagebox.show(Uri error)

ModuleManager load complete event still set, so on adminaccess.xap finish it recalls navigation a 3rd time, finds the right view to show and then displays correctly.

So I think I just need a way to register which module has completed loading and if x module is done loading then fire my event. I know you could use a sample application to help me and if it comes to that I'll set one up tonight, but I'm pretty sure I figured out the main issue.

I'm I setting something up wrong with this?

moduleManager.LoadModuleCompleted += (r, s) => regionManager.RequestNavigate(KnownRegions.MainContentRegion, uri);

Jan 23, 2012 at 4:04 PM

Yep that was the issue, other modules finishing load would call the navigation before it should have been. Just gotta create some more business logic and should be set.
Thank you for the support ^^

May 17, 2012 at 6:13 PM

Here's my solution... (it just works)

    public class FrameContentLoader : DependencyObject, INavigationContentLoader
    {
        private IRegion _region;
        private IRegionNavigationContentLoader _targetHandler;

        public String RegionName
        {
            get { return (String)GetValue(RegionNameProperty); }
            set { SetValue(RegionNameProperty, value); }
        }

        public static readonly DependencyProperty RegionNameProperty =
            DependencyProperty.Register("RegionName", typeof(String), typeof(FrameContentLoader), null);

        public Boolean CanLoad(Uri targetUri, Uri currentUri)
        {
            return true;
        }

        public void CancelLoad(System.IAsyncResult asyncResult)
        {
        }

        public System.IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, Object asyncState)
        {
            EnsureRegionIsSet();

            object view = null;
            if (!TryLoadContent(this._region, targetUri, out view))
            {
                var moduleCatalog = ServiceLocator.Current.GetInstance<IModuleCatalog>();
                string[] uriWords = targetUri.ToString().Split('.');

                ModuleInfo mi = moduleCatalog.Modules.First(m => uriWords.Contains(m.ModuleName));

                if (mi != null)
                {
                    Func<Uri> findView = () =>
                    {
                        string viewName = uriWords[uriWords.Length - 1];
                        Type viewType = Type.GetType(mi.ModuleType).Assembly.GetTypes().First(t =>
                            (t.Name == viewName) && typeof(UIElement).IsAssignableFrom(t.BaseType));

                        Uri uri = new Uri(String.Join(".", viewType.Namespace, viewType.Name), UriKind.Relative);
                        return (viewType != null && TryLoadContent(this._region, uri, out view)) ? uri : null;
                    };
                    if (mi.State == ModuleState.Initialized)
                    {
                        targetUri = findView() ?? targetUri;
                    }
                    else
                    {
                        //We found the Module, so grab the moduleManager and tell it to load it.
                        var moduleManager = ServiceLocator.Current.GetInstance<IModuleManager>();
                        var asyncResult = new LoadAsyncResult(userCallback, asyncState, false);
                        moduleManager.LoadModuleCompleted += (s, e) =>
                            {
                                if (e.Error == null)
                                {
                                    if (!TryLoadContent(this._region, targetUri, out view))
                                        targetUri = findView() ?? targetUri;
                                }
                                asyncResult.Complete(GetFrameContent(view, this._region), view, false);
                            };
                        moduleManager.LoadModule(mi.ModuleName);
                        return asyncResult;
                    }
                }
            }
            var result = new LoadAsyncResult(userCallback, asyncState, true);
            result.Complete(GetFrameContent(view, this._region), view, true);
            return result;
        }

        public LoadResult EndLoad(IAsyncResult asyncResult)
        {
            var loadAsyncResult = asyncResult as LoadAsyncResult;
            return new LoadResult(loadAsyncResult.FrameContent);
        }

        private void EnsureRegionIsSet()
        {
            if (this._region == null)
            {
                this._region = ServiceLocator.Current.GetInstance<IRegionManager>().Regions[this.RegionName];
                this._targetHandler = ServiceLocator.Current.GetInstance<IRegionNavigationContentLoader>();
            }
        }

        private bool TryLoadContent(IRegion region, Uri targetUri, out object view)
        {
            try
            {
                view = this._targetHandler.LoadContent(region, new NavigationContext(null, targetUri));
                return (view != null && typeof(UIElement).IsAssignableFrom(view.GetType().BaseType));
            }
            catch (InvalidOperationException) { view = null; return false; }
        }

        internal static DependencyObject GetFrameContent(Object view, IRegion region)
        {
            var frameworkElement = view as FrameworkElement;
            var content = frameworkElement ?? new ContentControl
            {
                Content = view,
                HorizontalAlignment = HorizontalAlignment.Stretch,
                VerticalContentAlignment = VerticalAlignment.Stretch
            };
            if (content is Page)
            {
                return content;
            }
            return content.Parent ?? new FrameNavigationWrapperPage
            {
                Content = content,
                FrameRegionNavigationService = region.NavigationService as FrameRegionNavigationService
            };
        }

        class LoadAsyncResult : IAsyncResult
        {
            private readonly AsyncCallback callback;
            private readonly object syncRoot;
            private readonly WaitHandle waitHandle;
            private bool completed;

            internal LoadAsyncResult(AsyncCallback cb, object state, bool completed)
            {
                this.callback = cb;
                this.completed = completed;
                this.AsyncState = state;
                this.CompletedSynchronously = completed;
                this.waitHandle = new ManualResetEvent(completed);
                this.syncRoot = new object();
            }

            public Object AsyncState { get; private set; }
            public Object FrameContent { get; private set; }
            public Object ActualView { get; private set; }

            public WaitHandle AsyncWaitHandle
            {
                get { return waitHandle; }
            }

            public Boolean CompletedSynchronously { get; private set; }

            public Boolean IsCompleted
            {
                get
                {
                    lock (this.syncRoot)
                        return this.completed;
                }
            }

            internal void Complete(object frameContent, object view, bool completedSynchronously)
            {
                lock (this.syncRoot)
                {
                    this.FrameContent = frameContent;
                    this.ActualView = view;
                    this.completed = true;
                }
                SignalCompletion();
            }

            private void SignalCompletion()
            {
                ((ManualResetEvent)this.waitHandle).Set();
                if (this.callback != null) { this.callback(this); }
            }
        }
    }