This project is read-only.

ViewService (NavigationService, DialogueService) for MVVM

Jun 1, 2013 at 1:19 AM
I'd like to use MVVM and ViewService pattern with Modern UI for WPF.
But I don't know how to implement NavigationService of Modern UI.
Does Modern UI provide NavigationService ?
Jun 1, 2013 at 9:58 PM
it does not provide a NavigationService.
Jun 2, 2013 at 1:32 AM
Edited Jun 2, 2013 at 2:09 AM
Seeing there's a lot of questions around a navigation service.

Will share what I'm doing, but note this a rudimentary implementation of a navigation service using mvvmlight, it really depends on what you want from a navigation service

My interface

namespace XXX..Interface
{
using System;

public interface INavigationService
{
    void Navigate<T>(object parameter = null);

    void GoBack();
}
}

My Navigation Service

namespace XXX.Service
{
using XXX.Interface;
using XXX.ViewModel;
using FirstFloor.ModernUI.Windows.Controls;
using FirstFloor.ModernUI.Windows.Navigation;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

public class NavigationService : INavigationService
{

    /// <summary>
    /// The view model routing.
    /// </summary>
    private static readonly Dictionary<Type, string> viewModelRouting 
        = new Dictionary<Type, string>
            {
                { typeof(AccountViewModel), "/View/Administration/AccountView.xaml" },
            };


    ModernFrame mainFrame;

    public NavigationService()
    {
        EnsureMainFrame();
    }

    private void EnsureMainFrame()
    {
        if (mainFrame == null)
        {
            var f = Application.Current.MainWindow;
            mainFrame = GetDescendantFromName(f, "ContentFrame") as ModernFrame;
        }
    }

    /// <summary>
    /// Gets the name of the descendant from.
    /// </summary>
    /// <param name="parent">The parent.</param>
    /// <param name="name">The name.</param>
    /// <returns>Gets a descendant FrameworkElement based on its name.A descendant FrameworkElement with the specified name or null if not found.</returns>
    private static FrameworkElement GetDescendantFromName(DependencyObject parent, string name)
    {
        int count = VisualTreeHelper.GetChildrenCount(parent);

        if (count < 1)
            return null;

        FrameworkElement fe;

        for (int i = 0; i < count; i++)
        {
            fe = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
            if (fe != null)
            {
                if (fe.Name == name)
                    return fe;

                fe = GetDescendantFromName(fe, name);
                if (fe != null)
                    return fe;
            }
        }

        return null;
    }

    /// <summary>
    /// Navigates the specified parameter.
    /// </summary>
    /// <typeparam name="T">ViewModel type</typeparam>
    /// <param name="parameter">The parameter.</param>
    public void Navigate<T>(object parameter = null)
    {
        EnsureMainFrame();

        var navParameter = string.Empty;
        if (parameter != null)
        {
            navParameter = "?param=" + JsonConvert.SerializeObject(parameter);
        }

        if (viewModelRouting.ContainsKey(typeof(T)))
        {
            Uri newUrl = new Uri(viewModelRouting[typeof(T)] + navParameter, UriKind.Relative);
            Uri oldUrl = mainFrame.Source;
            mainFrame.Navigate(oldUrl, newUrl, NavigationType.New);
        }
    }

    /// <summary>
    /// Invokes the go back.
    /// </summary>
    public void GoBack()
    {
        NavigationCommands.BrowseBack.Execute(null, null);   
    }
}
}

Since I'm using mvvmlight, below is how I would get an instance of the navigation Service, after I registered it, and navigate to the view. Note I have a one to one mapping of my view to my viewmodel.
        var x = SimpleIoc.Default.GetInstance<INavigationService>();
        x.Navigate<AccountViewModel>(customParamsInstance);

This is how I would go back.
            var x = SimpleIoc.Default.GetInstance<INavigationService>();
            x.GoBack();

But I still feel there should be some design considerations on exposing relevant navigation methods as public, that would make the ModernFrame navigation simpler.

Regards
PrakashZa
Jun 23, 2013 at 6:30 PM
Edited Jun 23, 2013 at 10:28 PM
Hey!

I have been struggling all day to get a handle to the NavigationService in my UserControls ViewModel. Your approach seems interesting but unfortunately I cant get the NavigationService class code to compile. The problem is this in the Navigate<T> method
 mainFrame.Navigate(oldUrl, newUrl, NavigationType.New);
The MUI's ModernFrame.Navigate() method is private and cannot be accessed.

I have been trying this in MUI 1.0.3 and 1.0.4 with no luck. Any suggestions?
Jun 24, 2013 at 9:35 AM
Edited Jun 24, 2013 at 1:30 PM
Yeah,

Supposedly you are using the MUI source code and not a nuGet pull, all you have to do is that change the Navigate method in ModernFrame class from private to public in the MUI source code, and that should solve it.

I did summit a issue to convert the method from private to public, as it shouldn't have any impact, maybe it will be public in the next release.

Regards
PrakashZa
Jun 24, 2013 at 10:12 PM
Hello again!

I took the MUI 1.0.4 source and made the ModernFrame.Navigate() public and then I was in business again :-) Thanks alot!

And now I am kind of stuck with how to retrieve the parameter in the navigated to viewmodel. I can get it sort of working with creating a constructor on the viewmodel taking the parameter in and using the Navigate like this:
var navigator = SimpleIoc.Default.GetInstance<INavigationService>();
navigator.Navigate<CustomerSearchResultViewModel>(new CustomerSearchResultViewModel(Customers));
However this results in both of the viewmodels two constructors (have to have a parameterless one too) being called - I dont like that. I am aware that this is on the side of this discussion but I thought I would give it a try ;-)

I am sure you have figured this one out?

Thanks again!
Jun 25, 2013 at 11:04 AM
Edited Jun 25, 2013 at 11:06 AM
If I look at your code, it not good practice to pass viewmodels to constructors. Always use some dependency injection for decoupling like NInject, SimpleIoc (Mvvmlight), AutoFac, Unity, there's tons out, and all of them implement constructor injection with no effort especially as your project gets bigger this will make life easier.

OK here a suggestion on how you would fix your viewmodel design a bit. Note the code below is written on the cuff in this response.

Create a the params class, so you would create your paramobject like below

public class CustomerParam
{
 public string Id;
 public string Mode;
 public string Name;
}

// So your navigation will be something like below in the viewmodel

var navigator = SimpleIoc.Default.GetInstance<INavigationService>();
navigator.Navigate<CustomerSearchResultViewModel>(new CustomerParam { Id = "xxx" } );

In your CustomerSearchResultView.

public class CustomerSearchResultView : FirstFloor.ModernUI.Windows.IContent
{
    // so as this view implements IContent, you will get the navigation events fired for navigated to/from and on
    // you need to decide which events(s) suits your purpose and manage it
    public void OnFragmentNavigation(FragmentNavigationEventArgs e)
    {
    }
    public void OnNavigatedFrom(NavigationEventArgs e)
    {           
    }
   // for example, OnNavigatedTo should be called when you navigated to the CustomerSearchResultView
    public void OnNavigatedTo(NavigationEventArgs e)
    {
              // for example here I would
              // step 1) check e if is typeof (CustomerParam) or contains a CustomerParam
              // step 2) if it does then I would 
                           // step 2.1) Load a busy 
                           // step 2.2) var custResultVM = SimpleIoc.Default.GetInstance<CustomerSearchResultViewModel>(); // nothing wrong with this
                           // step 2.3) custResultVM.DoWork( CustomerParam ); // me guess you will goto a db or something here, and your view bindings will show your results
                           // step 2.4) unload busy
              // step 3) if nothing was found, or params not understood then gracefully exit, with some logging

    }
    public void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
    }
}

Hope this guides you.

Regards
PrakashZa
Jun 25, 2013 at 2:15 PM
Ok - I see that iI should have elaborated a bit more on my draft code sample. This is how I use the navigationservice now
navigator.Navigate<CustomerSearchResultViewModel>(Customers);
"Customers" is a list of customers already searched and found like this: List<Customer>(). The CustomerSearchResultViewModel (and corresponding view) purpose is to display this list, and in that view I will implement some kind of customer master/detail logic.

Problem now is that the FirstFloor.ModernUI.Windows.Navigation.NavigationEventAgs seems very rudimentary. Whereas the System.Windows.Navigation.NavigationEventAgs has the "ExtraData" property where the parameters are held, this is lacking from the FirstFloor implementation.

So now status is that I use the navigator like shown above and am strugglig to find away to get those data out of the FirstFloor.ModernUI.Windows.Navigation.NavigationEventArgs passed to my code behind file. And I just now discovered that the typeof(e) show that I am dealing with a CustomerSearchResultViewModel type - and not the List<Customer> I would have expected ...

But thanks alot for taking the time to answer my questions!
Jun 25, 2013 at 8:15 PM
Sure no problem dude,

What I also wanted to share for params and not sure if it can assist your case, is to use the mvvmlight event to command. Your xaml for search button in your main search view will look like something like below.

http://geekswithblogs.net/lbugnion/archive/2009/11/05/mvvm-light-toolkit-v3-alpha-2-eventtocommand-behavior.aspx

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"

<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="OnClicked">
    <cmd:EventToCommand Command="{Binding Mode=OneWay, Path=SearchCommand}" 
                                         CommandParameter="{Binding Text, ElementName=MyTextBox, Mode=OneWay}"
                        PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>

Here the relay command SearchCommand is invoked on the button OnClicked event plus the text of the textbox is passed as a parameter to the SearchCommand. Pretty neat right !!! This works for listbox, image, pretty much any control, just get your bindings right.

While code flow is still in the viewmodel all the time, do your processing for the SearchCommand, then get an instance to the navigation service, and navigate somewhere. And since your navigation service is decoupled already with the pattern above you ok.

Lets me know if this helps.

Regards
PrakashZa
Jun 25, 2013 at 9:42 PM
Thanks for prompt reply mate!

I got around this so far by rewriting the Navigator service to build up the newUrl like this - from:
navParameter = "?param=" + JsonConvert.SerializeObject(parameter);
to:
navParameter = "#" + JsonConvert.SerializeObject(parameter);
because when doing this the FirstFloor OnFragmentNavigation event got triggered in the code behind - and in this event the passed parameter could be obtained. My codebehind basically looks like this now:
        private CustomerSearchResultViewModel _searchResult;
        public CustomerSearchResult()
        {
            InitializeComponent();
            _searchResult = SimpleIoc.Default.GetInstance<CustomerSearchResultViewModel>();
        }

        public void OnFragmentNavigation(FragmentNavigationEventArgs e)
        {
            var customers = e.Fragment;
            _searchResult.Customers = customers;
         }
I now have a Json string that is quite easily deserialised back to a List<Customers> in my viewmodel.

BUT - I am still using the codebehind and your apporoach looks very interesting. I will try that tomorrow. The less code in the codebehind, the better.
Jun 26, 2013 at 10:51 PM
OK cool, so you running nicely. Yeah I know about the "#", I did not go that route because I set a principle in my head to only rely on the MUI framework for its beautiful beautiful templates, and didn't want any other dependencies from MUI, anyway that's just my choice.

And yeah there's always a debate about how much code should reside in the view or viewmodel, to me good abstraction and implementation always prevails, don't like looking at code where I see confusing from the developer in his coding. In my MUI app I only use the IContent implementation on the view only for dialog management to register and un-register listeners, for which I rely heavily on the mvvmlight MessengerInstance, ;-) I call it my app service bus. Cheers mate.

Regards
PrakashZa
Sep 4, 2013 at 12:02 PM
Hi,

I have implemented the navigation service above in my application and whilst I am able to navigate to different pages successfully when I do so I am not seeing the change in navigation reflected in the menu, e.g. bold link.

I am not sure if this is due to the way that I have implemented the service or if its out of its scope.

Regards

Donald
Nov 19, 2013 at 7:58 PM
dparsons,

Not sure if this is the right approach but I made a small change to the code for Navigate<T> which seems to reflect the navigational changes in the menu. I might end up with other issues with this change so would like Prakashza to review this and comment.
//In Navigate<T> replace
//mainFrame.Navigate(oldUrl, newUrl, NavigationType.New);
//by
NavigationCommands.GoToPage.Execute(newUrl, null);
Regards,
Sanjeev
Nov 20, 2013 at 9:18 AM
Yeah I reverted back to that as well,

Stick to the MUI framework for MUI operations as much as possible to minimise impact on future changes/fixes from the framework. You should really see Navigation<T> more as a wrapper to the existing built in navigation. When I get a chance I will publish an update to a Navigation<T>.

Regards
PrakashZa
Jan 17, 2014 at 9:08 PM
Hi,
any chance that you can publish your updated navigationservice?
thanks,
David
Jan 17, 2014 at 9:13 PM
sanpan123 wrote:
dparsons,

Not sure if this is the right approach but I made a small change to the code for Navigate<T> which seems to reflect the navigational changes in the menu. I might end up with other issues with this change so would like Prakashza to review this and comment.
//In Navigate<T> replace
//mainFrame.Navigate(oldUrl, newUrl, NavigationType.New);
//by
NavigationCommands.GoToPage.Execute(newUrl, null);
Regards,
Sanjeev
I had better success passing the mainFrame value into the GoToPage method using:

NavigationCommands.GoToPage.Execute(newUrl,mainFrame);
Jan 17, 2014 at 10:45 PM
dgjohnson wrote:
sanpan123 wrote:
dparsons,

Not sure if this is the right approach but I made a small change to the code for Navigate<T> which seems to reflect the navigational changes in the menu. I might end up with other issues with this change so would like Prakashza to review this and comment.
//In Navigate<T> replace
//mainFrame.Navigate(oldUrl, newUrl, NavigationType.New);
//by
NavigationCommands.GoToPage.Execute(newUrl, null);
Regards,
Sanjeev
I had better success passing the mainFrame value into the GoToPage method using:

NavigationCommands.GoToPage.Execute(newUrl,mainFrame);
In this case mainFrame would be the context in which you will perform the navigation,

Regards
PrakashZa
Jan 18, 2014 at 12:15 AM
Edited Jan 18, 2014 at 1:18 AM
Hi,
I seem to still be having some issues with navigation menu syncing.

If I provide ANY parameter the mainWindow links do not sync...
if I pass a null parameter then the links all sync...

I think it is because the uri does not match(with the parameter data) the url of my modern tab link sources:
                <mui:Link DisplayName="performances" Source="/Pages/Performances.xaml" />
                <mui:Link DisplayName="friends" Source="/Pages/Friends.xaml" />
                <mui:Link DisplayName="store" Source="/Pages/Store.xaml" />
but im not sure how to resolve that issue.
any idea?

below is the method my relay command is calling
        public void NavigateToEdit()
        {
            navigationService.Navigate<FriendsViewModel>(new NavigationParameter() { Id = friend.Id.ToString() });
        }
EDIT:
it was a case of ModernMenu not finding a match due to the fragment on the URI.

to resolve the issue I modified ModerMenu.cs as follows:

add new method to ModermMenu.cs
        private Uri CleanUri(Uri source)
        {
            if (source == null)
                return null;

            var url = source.OriginalString.Split('#');
            return new Uri(url[0],UriKind.Relative);
        }
the clean the Uri before it is used to find the match in ModernMenu.UpdateSelection()
        private void UpdateSelection()
        {
            LinkGroup selectedGroup = null;
            Link selectedLink = null;

            Uri cleanSelectedSource = CleanUri(this.SelectedSource);

            if (this.LinkGroups != null) {
                // find the current select group and link based on the selected source
                var linkInfo = (from g in this.LinkGroups
                                from l in g.Links
                                where l.Source == __cleanSelectedSource
                                select new {
                                    Group = g,
                                    Link = l
                                }).FirstOrDefault();

                if (linkInfo != null) {
                    selectedGroup = linkInfo.Group;
                    selectedLink = linkInfo.Link;
                }
                else {
                    // could not find link and group based on selected source, fall back to selected link group
                    selectedGroup = this.SelectedLinkGroup;

                    // if selected group doesn't exist in available groups, select first group
                    if (!this.LinkGroups.Any(g => g == selectedGroup)) {
                        selectedGroup = this.LinkGroups.FirstOrDefault();
                    }
                }
            }
--------------method truncated for brievety-------------