Am I fighting the framework (WP7/Silverlight) with Navigate(Type)

Sep 21, 2010 at 3:18 PM

Hi,

first of all, I really like your framework, and I am planning to use it in some mid-sized project.

Second: I hate the string XAML navigation, things like:

 

navigationService.Navigate(new Uri("/Pages/PageTwo.xaml?NumberOfTabs=5", UriKind.RelativeOrAbsolute));

 

 

 

So I created my own little extension to the navigation service, looking like this:

 

 

 public bool Navigate(Type targetType)
        {
            logger.Info("Navigate called type " + targetType);
            
            string appNs = Application.Current.GetType().Namespace;
            string targetTypeNs = targetType.Namespace;
            
            // Make sure page is in the same namespace
            if (!targetTypeNs.StartsWith(appNs))
                throw new Exception("Can't auto resolve foreign controls for now");

            string target = targetTypeNs.Remove(0, appNs.Length);
            target = target.Replace('.', '/')  + '/' + targetType.Name + ".xaml";

            return Navigate(new Uri(target, UriKind.Relative));
        }

 

Do you have any feedback on this? I skipped the argument part for now, I think I will create a small parser for that, too. Is there any reason for using this stupid string based XAML navigation I do not know about? Of course, with this the namespaces have to match folder names, but my Resharper supports this perfectly, so there is no problem in that...

Its not really a bug, more of a feature I am wondering why you did not create it? Perhaps you don't hate strings so much, or there is some support in Blend I do not know about, which I would break using the approach above. With this approach, your example changes to (without args at the moment):

 

   public void GotoPageTwo()
        {
            //navigationService.Navigate(new Uri("/Pages/PageTwo.xaml?NumberOfTabs=5", UriKind.RelativeOrAbsolute));
            navigationService.Navigate(typeof(PageTwo));
        }

 

 

Coordinator
Sep 21, 2010 at 3:51 PM

I would probably actually do something like this in my own projects. I just didn't implement it in the framework...yet. If you can come up with a compelling, simple way to handle the parameters...I'll definitely consider adding this :)

Sep 21, 2010 at 9:15 PM

Ok,

I created a really simple parameter handling. Basically I wrote a helper class using Reflection to check if the desired parameters exist. This check is done only if DEBUG is set, providing some nice error messages when creating an application. There are some limitations at this point:

  • Only supports basic types (string, int, long)
  • Encoded the params to a string (perhaps a global lookup would be nicer)
  • Is not fully tested for each case

But it works, and has minimal implications on your code. The usage in the ViewModel is:

  public class MainPageViewModel
    {
        readonly INavigationService navigationService;
        private static readonly ILog Log = LogManager.GetLog(typeof(MainPageViewModel));
        private static int counter = 1;
        
        // Will throw invalid payload exception
        // private static NavTarget pageTwoLink = NavTarget.ToPage<PageTwo>("NumberOfTabs", new Button());

        // Will throw property not found exception
        // private static NavTarget pageTwoLink = NavTarget.ToPage<PageTwo>("NumberOfTabsX", 5);
        
        // Will work
        // private static NavTarget pageTwoLink = NavTarget.ToPage<PageTwo>("NumberOfTabs", 5);

        // Using more then one argument
        private static NavTarget pageTwoLink = NavTarget.ToPage<PageTwo>("NumberOfTabs", 5).Add("StartItem", 2);

        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
            Log.Info("Created instance " + GetType() + " " + counter++);
        }

        public void GotoPageTwo()
        {
            navigationService.Navigate(pageTwoLink);
        }
    }


The function I added to "your code" is:

  /// <summary>
        /// Navigates to the specified <see cref="Uri"/>.
        /// </summary>
        /// <param name="targetType">The Page type to navigate to.</param>
        /// <returns>Whether or not navigation succeeded.</returns>
        public bool Navigate(NavTarget navTarget)
        {
            logger.Info("Navigate called type " + navTarget.NavigationTargetType);
            var navString = NavTarget.CreateNavStringFromType(navTarget.NavigationTargetType);

            if (navTarget.NavigationParams != null)
                navString += NavParams.EncodeParamsToString(navTarget.NavigationParams);

            return Navigate(new Uri(navString, UriKind.Relative));
        }

The two classes I created would blow up this post to much.. Should I send them to you? You can also find them here>

http://randombytes.de/codeplex/attachments/NavParam.cs

http://randombytes.de/codeplex/attachments/NavTarget.cs

 

Pretty basic stuff, but it works and removes some stupid typo errors and supports refactoring. I might even use a ClassName.PropString(x => x.NumberOfTabs) instead of the string value in the example above, to support even renaming of Properties. But this is over the top for now, and adds possible overhead, I think we should keep it out of the source...

Chris

 

Coordinator
Sep 21, 2010 at 11:56 PM

Chris, thanks for working on this. I'm adding a ticket to remind me to dig into your code deeper and see about adding this in.

Sep 22, 2010 at 7:59 AM
Edited Sep 22, 2010 at 8:40 AM

Here's a short simple "code as data" way to do this. It gives us strongly-typed target and query string args. Magic strings, be gone!. The implementation would go alongside the current public bool Navigate(Uri source) {...}

public bool Navigate<T>(Expression<Func<T>> typeInitializer)
{
    var initExpression = typeInitializer.Body as MemberInitExpression;

    if (initExpression == null) throw new ArgumentException("The typeInitializer's Body must be a MemberInitExpression (e.g. ()=> new GoodViewModel {Id=42}).");

    var parts = initExpression.Bindings.Select(x => x.Member.Name + "=" + ((ConstantExpression)((MemberAssignment)x).Expression).Value);
    var targetType = typeInitializer.Body.Type;
    var rootNamespace = Application.Current.GetType().Namespace;
    var target = String.Format("{0}/{1}.xaml",
        targetType.Namespace.Remove(0, rootNamespace.Length).Replace(".", "/"),
        targetType.Name.Replace("ViewModel", "View"));

    if (parts.Any())
    {
        target += parts.Aggregate("?", (x, y) => (x == "?") ? x + y : x + "&" + y);
    }
    return Navigate(new Uri(target, UriKind.RelativeOrAbsolute));
}

The usage would be: 
    
navigationService.Navigate(() => new PersonViewModel{ GadgetId = 42, Twitter = "bryan_hunter"});

It uses lambda expressions (in particular a MemberInitExpression) to let the caller specify what type (ViewModel) they're interested in and what properties need to be passed via query string. Note: it doesn't actually new-up the type-- this is "code as data". 

@Rob, does this qualify as a "compelling, simple way to handle the parameters"?


Coordinator
Sep 22, 2010 at 1:03 PM
Edited Sep 22, 2010 at 1:04 PM

That looks pretty darn good. I'm trying to decide whether or not to add this into the framework or make it available as a recipe since it could be bolted on pretty easy using an extension method.

Sep 22, 2010 at 2:23 PM

Yep, I wondered that too. I like the recipe idea. In my WPF projects I would never use this addition. In my WP7 projects I would-- like @ChristianR I really, really hate those Uri strings. 

If you add it, you might consider just targeting WP7 (maybe in the FrameAdapter). The view-first stuff for WP7 is nasty and yanks away a lot of the "Caliburn fun" of developing there. This would help. 

I'm working on a phone conductor that will hopefully allow the shell to be the only view-first part and everything else to work in a good, grown-up, Caliburn-VM-first way. I was derailed a bit by the tombstoning changes in RTM, but I'm back on task now. When I'm done I'll share it (unless it stinks).

@ChristianR, I really appreciate you starting this discussion. I'm sure I would have continued "grumbling but tolerating" the Uri mess indefinitely without your prodding. ¡Viva la Revolución!

I'm drafting a blog post for the Caliburn-less about adding a NavigationService extension method with the query string goodies above. They will still have to wire-up the querystring-catching stuff themselves, but it will still help. I'll link back to this topic from that post. 

Sep 23, 2010 at 12:38 PM

Yeah, when I moved to C# a few years ago, I really liked all the strong typing. Compared to e.g. the PHP mess you can create in a few hours of careless writing. Then I found Resharper, and everything got even better (refactoring, meaning you can try a lot quick and dirty, and rename later).

Then came Silverlight and XAML (not sure if it is that horrible in WPF, too) and the "Return of the string" :-)

I mean they are maturing very quickly, I can remember the times where the designer in VS for XAML was readonly, and crashed from time to time. Now Blend really looks nice, but there is still some room for improvements. I honestly do not understand why this URI navigation has no built in support for strong typing. I think all this string focus is due to XAML and Blend. They are nice, but I do not want to see any string in my .cs files :-)

So I will definitely be using your framework the next weeks and perhaps give some more input. Oh, and your code looks really much shorter then mine. Damn... Have to learn some of this neat short things like   target += parts.Aggregate("?", (x, y) => (x == "?") ? x + y : x + "&" + y); - I knew the substring(0, string.len) and other stuff was hurting everyones eyes :-)


Cheers,

Chris