Navigation using Object as parameter !!

Topics: Prism v4 - Silverlight 4
Mar 5, 2011 at 8:32 PM

Dear All

Is it possible to use the navigation parameter to pass objects to navigated page ? if yes, how is it possible ?

Appreciate any feedback

Regards

Waleed

Developer
Mar 9, 2011 at 2:02 PM

Hi Waleed,

The scenario you're mentioning isn't possible in Prism out of the box. You might find the following thread handy, in which a similar concern is covered.

I hope you find this helpful.

Guido Leandro Maliandi
http://blogs.southworks.net/gmaliandi

Apr 7, 2011 at 11:27 AM

Hi Waleed,

You can pass parameters as objects, however there is a little bit more work involved. I really feel that the default method of passing parameters by using the query string in Prism is one of it's biggest weaknesses. Anyway, you can do something like the following:

In a project I am working on at the moment, I have an ICommand like this:

    Public Class ShowClientsModuleCommand
        Implements ICommand

        Public Function CanExecute(parameter As Object) As Boolean Implements System.Windows.Input.ICommand.CanExecute
            Return True
        End Function

        Public Event CanExecuteChanged(sender As Object, e As System.EventArgs) Implements System.Windows.Input.ICommand.CanExecuteChanged

        Public Sub Execute(parameter As Object) Implements System.Windows.Input.ICommand.Execute
            Dim regionManager As IRegionManager = ServiceLocator.Current.GetInstance(Of IRegionManager)()
            Dim navigatorUri As New Uri(GetType(ClientsNavigator).Name, UriKind.Relative)
            Dim workspaceUri As New Uri(GetType(ClientsSummary).Name, UriKind.Relative)

            regionManager.Regions(RegionNames.WorkspaceRegion).Context = parameter

            regionManager.RequestNavigate(RegionNames.NavigatorRegion, navigatorUri)
            regionManager.RequestNavigate(RegionNames.WorkspaceRegion, workspaceUri)
        End Sub

    End Class


The line that sets the Context of the region you are navigating to is what is important here (you don't have to use an ICommand to do this of course).

Then in the ViewModel for the UserControl that will be displayed in that region you will need to implement the INavigationAware interface and do something like the following:

        Public Sub OnNavigatedTo(navigationContext As Microsoft.Practices.Prism.Regions.NavigationContext) Implements Microsoft.Practices.Prism.Regions.INavigationAware.OnNavigatedTo
            mClient = CType(navigationContext.NavigationService.Region.Context, UI.ClientUI)
        End Sub

Seems way too hard, and it's hardly type-safe, but gets around Prisms rather obvious limitation in this area. Any other ideas are welcome.

Cheers,

Adam Valpied

May 2, 2011 at 8:29 PM

I implemented some extensions that incorporate Adam Valpied's suggestion - I think they make the functionality more obvious to developers that will use my code later on.  I created a class "NavigationParameters" that is almost identical to the UriQuery class, except instead of mapping strings to strings it maps strings to objects.  Here it is:

 

    /// <summary>
    /// Represents custom navigation parameters for PRISM region navigation.
    /// </summary>
    public class NavigationParameters : IEnumerable<KeyValuePair<string, object>>
    {
        private readonly List<KeyValuePair<string, object>> entries = new List<KeyValuePair<string, object>>();

        /// <summary>
        /// Initializes a new instance of the <see cref="NavigationParameters"/> class.
        /// </summary>
        public NavigationParameters()
        {
        }

        /// <summary>
        /// Gets the <see cref="System.Object"/> with the specified key.
        /// </summary>
        /// <value>The value for the specified key, or <see langword="null"/> if the query does not contain such a key.</value>
        public object this[string key]
        {
            get
            {
                foreach (var kvp in this.entries)
                {
                    if (string.Compare(kvp.Key, key, StringComparison.Ordinal) == 0)
                    {
                        return kvp.Value;
                    }
                }

                return null;
            }
        }

        /// <summary>
        /// Gets the enumerator.
        /// </summary>
        /// <returns>An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.</returns>
        public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            return this.entries.GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Adds the specified key.
        /// </summary>
        /// <param name="key">The item key.</param>
        /// <param name="value">The value.</param>
        public void Add(string key, object value)
        {
            this.entries.Add(new KeyValuePair<string, object>(key, value));
        }
    }

Then I created some extension methods for RegionManager and NavigationContext for requesting navigation and grabbing the objects off of the context.  You can create similar methods to suit your needs.

 

    /// <summary>
    /// Extends functionality of the region navigation in PRISM.
    /// </summary>
    public static class NavigationExtensions
    {
        #region RegionManager Extensions

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="regionManager">The region manager.</param>
        /// <param name="regionName">Name of the region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationCallback">The navigation callback.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegionManager regionManager, string regionName, Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters)
        {
            if (regionManager == null)
            {
               navigationCallback(new NavigationResult(new NavigationContext(null, target), false));
                return;
            }

            if (regionManager.Regions.ContainsRegionWithName(regionName))
            {
                regionManager.Regions[regionName].RequestNavigate(target, navigationCallback, navigationParameters);
            }
            else
            {
               navigationCallback(new NavigationResult(new NavigationContext(null, target), false));
            }
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegionManager regionManager, string regionName, string target, NavigationParameters navigationParameters)
        {
            RequestNavigate(regionManager, regionName, new Uri(target, UriKind.RelativeOrAbsolute), (nr) => { }, navigationParameters);
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationCallback">The navigation callback.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegionManager regionManager, string regionName, string target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters)
        {
            RequestNavigate(regionManager, regionName, new Uri(target, UriKind.RelativeOrAbsolute), navigationCallback, navigationParameters);
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegionManager regionManager, string regionName, Uri target, NavigationParameters navigationParameters)
        {
            RequestNavigate(regionManager, regionName, target, (nr) => { }, navigationParameters);
        }

        #endregion

        #region Region Extensions

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationCallback">The navigation callback.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegion region, Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters)
        {
            if (region == null)
            {
                return;
            }

            region.Context = navigationParameters;

            region.RequestNavigate(target, navigationCallback);
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegion region, string target, NavigationParameters navigationParameters)
        {
            RequestNavigate(region, new Uri(target, UriKind.RelativeOrAbsolute), (nr) => { }, navigationParameters);
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationCallback">The navigation callback.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegion region, string target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters)
        {
            RequestNavigate(region, new Uri(target, UriKind.RelativeOrAbsolute), navigationCallback, navigationParameters);
        }

        /// <summary>
        /// Requests the navigate.
        /// </summary>
        /// <param name="region">The region.</param>
        /// <param name="target">The target.</param>
        /// <param name="navigationParameters">The navigation parameters.</param>
        public static void RequestNavigate(this IRegion region, Uri target, NavigationParameters navigationParameters)
        {
            RequestNavigate(region, target, (nr) => { }, navigationParameters);
        }

        #endregion

        #region NavigationContext Extensions

        /// <summary>
        /// Gets the navigation parameters.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns>The NavigationParameters for the NavigationContext.</returns>
        public static NavigationParameters GetNavigationParameters(this NavigationContext context)
        {
            if (context == null)
            {
                return new NavigationParameters();
            }

            if (context.NavigationService == null || context.NavigationService.Region == null)
            {
                return new NavigationParameters();
            }

            if (context.NavigationService.Region.Context is NavigationParameters)
            {
                return context.NavigationService.Region.Context as NavigationParameters;
            }

            return new NavigationParameters();
        }

        #endregion
    }

May 3, 2011 at 3:54 PM

You could convert your object to a json string using either DataContractJsonSerializer of Newtonsoft Json.Net and then pass it as UriQuery and the deserialize it on the other end.

May 9, 2011 at 1:18 PM

Hi petunya,

I like your approach. Haven't used it obviously, but I think that should work quite well.

Gan_s, thanks for your suggestion, but this is why I do not like to do what you have suggested in a Window's application:

  • There is a lot more overhead involved than what is really necessary
  • The object that you end up with is not the same object that you began with - so you end up with two versions of the object. It just makes more sense to pass objects by reference in most applications like this.
  • Some objects that you want to pass around may contain read-only computed values, or properties that cannot be serialized easily - or at all.
  • You can only pass 1 or 2k worth of data in a query string - only enough for the simplest of objects.

I still do not like having to pass objects using the Region's context and that, along with a few other things, has caused me to stop using Prism. I feel that it still has a LONG way to go before it's ready for the type of commercial desktop applications that the company I work for develop, and the time you have to invest setting Prism up and learning its quirks is just too high. Still I plan to keep an eye on how it develops.

That's just my two cents worth though :)

May 9, 2011 at 8:21 PM

Hello Adam,

I guess you might consider the eventAgrregator to pass any type of objects (nested Object) if you want to.

Using Publish & Subscribe.

Regards

Waleed

May 10, 2011 at 12:36 AM

Hi Waleed,

You could do that, but it just seems so much harder than it should be. There are a number of ways to pass objects by reference around, but I think that any framework that requires this much work to do something so simple and critical for anything but the most basic of applications is not ready for commercial use. Personally I only use an EventAggregator to notify different parts of the application that a particular event has taken place - like a customer object being updated or something. I'm not sure it should be used to pass data around for navigation.

It's an interesting design choice they made with Prism as even the basic WPF Navigation Service, which also has its quirks, supports passing context to the page that you are navigating to.

Cheers,

Adam

May 10, 2011 at 3:07 AM
adamvalpied wrote:

Hi petunya,

I like your approach. Haven't used it obviously, but I think that should work quite well.

Gan_s, thanks for your suggestion, but this is why I do not like to do what you have suggested in a Window's application:

  • There is a lot more overhead involved than what is really necessary
  • The object that you end up with is not the same object that you began with - so you end up with two versions of the object. It just makes more sense to pass objects by reference in most applications like this.
  • Some objects that you want to pass around may contain read-only computed values, or properties that cannot be serialized easily - or at all.
  • You can only pass 1 or 2k worth of data in a query string - only enough for the simplest of objects.

I still do not like having to pass objects using the Region's context and that, along with a few other things, has caused me to stop using Prism. I feel that it still has a LONG way to go before it's ready for the type of commercial desktop applications that the company I work for develop, and the time you have to invest setting Prism up and learning its quirks is just too high. Still I plan to keep an eye on how it develops.

That's just my two cents worth though :)


Adam,

Firstly I thought your app was a silverlight app and not a windows app as you have posted it under "Prism 4 - Silverlight 4".

Also with json serialization/deserialization I dont see any overhead. A better serializer is the newtonsoft json serializer, which does not require your POCO classes to be decorated with DataContract and DataMember attributes. It is very efficient for json-object-json. So your point about "you end up with two versions of the object" is eliminated. You get back exactly what you serialized. But yes, you may right in saying that the query string might reject beyond a certain size.

If you ask me Prism is a best way to go for modularised applications, of course only if you use it the right way :), otherwise it could be messy to figure out issues. RegionContext is another good option as mentioned above.

May 10, 2011 at 9:53 AM

Hi gan_s,

Ah - didn't realize this was in a Silverlight forum - just stumbled upon it when I was searching for something else. I agree that serializing objects does not have a huge overhead - I just dislike having to work so hard to just pass data around - and not really an option if you are passing the serialized object in the query string.

I've seen a number of posts about this where people were using all sorts of 'hacky' measures to try to pass context around - like using global variables, sessions and the like - all of which used in this scenario are bad practice (especially since navigation is not synchronious - so you could end up grabbing the wrong context in the examples I saw). I feel that it's interesting that this framework comes under Microsoft's 'patterns & practices' banner and yet makes it very difficult for users to actually use 'best practice' for things as simple as passing data from one part of the application to another.

RegionContext was good - but I did not find it helpful in the following scenario:

  • You have a region with a list of customers for example
  • When you click on a customer you want to show that customer in an editing region (in my example I also had another region showing summary info for the customer underneath the list).
  • But if the user has modified the previously selected customer object you want to show a confirmation dialog asking if the user wants to continue and lose their changes.
  • If the customer selects 'No' then you don't want to set the RegionContext of all regions in scope - but it's too late of course.

I agree with the general principle of Prism - and generally code using MVP or MVVM so it fits quite nicely with my usual approach to things - I just don't think it's ready for the real world yet. But I'm happy to be proven wrong :)

 

May 10, 2011 at 11:28 PM

I think the decision of having uri string parameters is not a bad one since we are talking about a modular framework where views can be plugged in and out anywhere (region). It makes the navigation simple.

In LOB applications, you will most probably have a service layer / data repository layer to access your data whether its from isolated storage, in memory cache or web services. When you go from one view to another, you can pass the ID in the navigation URI, and the target view can get the object against that ID from your service/repository layer.

so, I dont think this "issue" will make me not use Prism for projects. Its a great framework and will improve further with subsequent releases.

May 11, 2011 at 3:23 AM

Absolutely echo that !! Prism has much more to offer !!

Aug 5, 2011 at 1:32 PM

The workaround I use is to create a class that inheirts the Uri class and has a property called 'Parameters' that returns a Dictionary<string,object>. I then pass an instance of this class to the RequestNavigate as the uri. In my view model, I cast the Uri of the NavigationContext to my subclassed Uri and from there I access the 'Parameters' property.

 

E.x.:

My Uri (I'm using MEF with exported views, so the full type name of the view is the url)

internal sealed class NavigationUri : Uri
{
    private NavigationUri(string uri)
        : base(uri, UriKind.Relative)
    {
        Parameters = new Dictionary<string, object>();
    }

    public static NavigationUri Create<TView>()
    {
        string uri = typeof(TView).FullName;
        return new NavigationUri(uri);
    }

    public Dictionary<string, object> Parameters
    {
        get;
        private set;
    }
}
Instead of hardcoding strings, I use a class to generate the parameter keys
    internal sealed class NavigationUriParameters
    {
        public static readonly string ViewText = Guid.NewGuid().ToString();
        public static readonly string ViewIcon = Guid.NewGuid().ToString();
    }

Here's the code snippet that creates the uri and does the navigation

var uri = NavigationUri.Create<ConfigurationSearchView>();
uri.Parameters[NavigationUriParameters.ViewIcon] = icon.Source;
uri.Parameters[NavigationUriParameters.ViewText] = "View Header Text Goes Here";
_regionMgr.RequestNavigate(Shell.ViewRegion, uri, Callback);

I can now retrieve the parameters from the NavigationContext from my viewmodel (The viewmodel implements IConfirmNavigationRequest)

public override void OnNavigatedTo(NavigationContext navigationContext)
{
    var uri = (NavigationUri)navigationContext.Uri;
    Text = (string)uri.Parameters[NavigationUriParameters.ViewText];
    Image = (ImageSource)uri.Parameters[NavigationUriParameters.ViewIcon];
}

Oct 13, 2011 at 9:01 AM

Thanks.