Search code examples
c#wpfmvvmcomboboxnavigation

Two questions about mvvm navigation of pages


I am trying to make a template-translator (.doc) from EN to other languages.

It is just for me.

I have already done simple mvvm navigation. For clear understanding what do I want, check picture:

UI

First question: How do I translate ICommand from button "NextItem" to current selected page that has changed a item inside textBox, otherwise how do I Call Translate() method from current page for my Button which is in the MainView?

Second question: How do I put all pages that I have on the Window in the Combobox on the Upper side window, and select page from there, like I do this with my Buttons.

How it is now:

<Button
    x:Name="ButtonSecondView"
    Width="200"
    Command="{Binding GoToSecondViewCommand}"
    Content="SecondView" />

<Button
    x:Name="ButtonNextItem"
    Grid.Row="2"
    Width="250"
    Command="{Binding NextRandomItem}"
    Content="Next item" />

MyCollection is just a stub which generates random items(1 item, 3 item, etc...). There I can translate some parameters to page while it is initializing.

public MainViewModel()
{
    MyCollection = new MyCollection();
    CurrentViewModel = new FirstViewModel(this,MyCollection.GetRandomItem());
    PageList = MyCollection.GetList();
}

public ICommand GoToFirstViewCommand
    {
        get
        {
            return new RelayCommand(() => { CurrentViewModel = new FirstViewModel(this, MyCollection.GetRandomItem()); });
        }
    }

public ICommand GoToSecondViewCommand
    {
        get
        {
            return new RelayCommand(() => { CurrentViewModel = new SecondViewModel(this, MyCollection.GetRandomItem()); });
        }
    }

ctor in SecondViewModel

public SecondViewModel(INotifyContentChanged contentChanged,string Parametrs)
{
    ContentChanged = contentChanged;
    TextContent = Parametrs;
}

One more time: First question.

I have many pages (in there 3), and I need to click the button on bottom, and in my page. In my current page I get text from textBox, and input these parameters to my method, like Translate(string field1). And this works on all pages that I want. If I change page in which I select Combobox items, I can do the same button click to button, and text from textBox inputted in my method Translate(string field1).


Solution

  • To navigate and pass the parameters to the corresponding page view models I stick to your pattern and used composition. I introduced a composition container that holds all page view models in a Dictionary<string, IPageViewModel>. Therefore all page view models must implement this interface. As the key I used the page view model's type name (e.g. nameof(FirstViewModel)). I also introduced a new property called PageNavigationParameter that binds to the TextBox in order to get the content (which is supposed to be passed to the corresponding page view model).

    A second Dictionary<string, string> maps the display name of each page view model (the page name to be displayed in the ComboBox) to the actual page view model name (that matches the class name e.g. nameof(FistViewModel)). This way you can get the desired page view model by class name or if in the navigation scope from the page display name.

    To select pages from a ComboBox you could do this:

    1. create a collection of page names in the view model and bind it to the ComboBox.ItemSource
    2. bind the ComboBox.SelectedItem property to the view model
    3. navigate to page when the view model's property changed

    To make this example work you need a common interface that all page view models must implement (e.g. class FirstViewModel : IPageViewModel). This interface must contain at least the PageNavigationParameter

    The page view model interface

    interface IPageViewModel
    {
      string PageNavigationParameter { get; set; }
    }
    

    Main view model (using composition)

    class MainViewModel
    {
        public MainViewModel()
        {
          // The Dictionary to get the page view model name 
          // that maps to a page display name
          this.PageViewModelNameMap = new Dictionary<string, string>()
          {
             {"First Page", nameof(FirstViewModel)},
             {"Second Page", nameof(SecondViewModel)}
          };
    
          // The ComboBox's items source
          // that holds the page view model display names
          this.PageNames = new ObservableCollection<string>(this.PageViewModelNameMap.Keys);
    
          // The Dictionary that stores all page view models 
          // that can be retrieved by the page view model type name
          this.PageViewModels = new Dictionary<string, IPageViewModel>()
          {
             {nameof(FirstViewModel), new FirstViewModel()},
             {nameof(SecondViewModel), new SecondViewModel()}
          };
    
          this.CurrentPageViewModel = this.PageViewModels[nameof(FirstViewModel)];
          this.PageNavigationParameter = string.Empty;
        }
    
        // You can use this method as execute handler 
        // for your NavigateToPage command too
        private void NavigateToPage(object parameter)
        {
          if (!(parameter is string pageName))
          {
            return;
          }
    
          if (this.PageViewModelNameMap.TryGetValue(pageName, out string pageViewModelName)
          {
            if (this.PageViewModels.TryGetValue(pageViewModelName, out IPageViewModel pageViewModel)
            {
              pageViewModel.PageNavigationParameter = this.PageNavigationParameter;
              this CurrentPageViewModel = pageViewModel;
            }
          }
        }
    
        private bool CanExecuteNavigation(object parameter) => parameter is string destinationPageName && this.PageViewModelNameMap.Contains(destinationPageName);
    
        private void OnSelectedPageChanged(string selectedPageName)
        {
          NavigateToPage(selectedPageName);
        }
    
        private ObservableCollection<string> pageNames;   
        public ObservableCollection<string> PageNames
        {
          get => this.pageNames;
          set 
          { 
            this.pageNames = value; 
            OnPropertyChanged();
          }
        }
    
        private string selectedPageName;   
        public string SelectedPageName
        {
          get => this.selectedPageName;
          set 
          { 
            this.selectedPageName = value; 
            OnPropertyChanged();
            OnSelectedPageChanged(value);
           }
         }
    
        private string pageNavigationParameter;   
        public string PageNavigationParameter
    
        {
          get => this.pageNavigationParameter;
          set 
          { 
            this.pageNavigationParameter= value; 
            OnPropertyChanged();
          }
        }
    
        private Dictionary<string, ViewModelBase> pageViewModels;   
        public Dictionary<string, ViewModelBase> PageViewModels
        {
          get => this.pageViewModels;
          set 
          { 
            this.pageViewModels = value; 
            OnPropertyChanged();
          }
        }
    
        private Dictionary<string, string> pageViewModelNameMap;   
        public Dictionary<string, string> PageViewModelNameMap
        {
          get => this.pageViewModelNameMap;
          set 
          { 
            this.pageViewModelNameMap = value; 
            OnPropertyChanged();
          }
        }
    
        private IPageViewModel currentPageViewModel;   
        public IPageViewModel CurrentPageViewModel 
        {
          get => this.currentPageViewModel;
          set 
          { 
            this.currentPageViewModel= value; 
            OnPropertyChanged();
           }
         }
      }
    

    The controls that have a cross page scope must have the MainViewModel as their DataContext.

    XAML snippet

    <!-- The page menu (DataContext is MainViewModel) -->
    <ComboBox SelectedItem="{Binding SelectedPageName}" ItemsSource="{Binding PageNames}" />
    
    <!-- The navigation parameter TextBox (DataContext is MainViewModel) -->
    <TextBox Text="{Binding PageNavigationParameter}" />
    

    For your navigation button commands you can use the same MainViewModel.NavigateToPage() method as the execute delegate handler and CanExecuteNavigation as the can execute handler. So you now have a single navigation command (e.g. NavigateToPage) that navigates to the destination page by passing the page display name as CommandParameter.