1

I'm trying to make a WPF MVVM application where there is one main window that has a sidebar with a button for each MVVM view and the rest of said window displays one view at a time.

Something like: enter image description here

(ignore that in this example the sidebar is at the top, this is just for illustrative purposes)

My goal is to be able to click a button and have the yellow part change to the view that corresponds with the button.

I can do it just fine hardcoded-ly like so:

<Button Command={Binding ChangeView} CommandParameter={Binding CalculatorView}/>

public FrameworkElement CurrentControlView { get; set; }

public ICommand ChangeViewCommand { get; }

// command initialization

void ChangeView(string viewName)
{
    Type viewType = Type.GetType($"Program.Views.{viewName}");
    CurrentControlView = (FrameworkElement)Activator.CreateInstance(viewType);
}

And bind what view is being shown via the CurrentControlView property.

However I'm trying to make it build itself. What I mean is, I'd like to write code that will look for all MVVM-convention views (XAML files with codebehinds) in a certain namespace and create a button that, when clicked, would fire code that would show its corresponding view.

So far I thought about using some sort of reflection code to gather the views, put them in a collection and have an ItemsControl bind to that collection something like so:

(again keep in mind in the picture the buttons are at the top but I want them at the left side)

    <ItemsControl ItemsSource="{Binding ViewsCollection}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Button Content="??" Command="{Binding ChangeViewCommand}" CommandParameter="??" HorizontalAlignment="Center" VerticalAlignment="Center" Height="40"/>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

However here I'm not sure what to put for Content as I want the button to display the name of the view. So if I have "CalculatorView.xaml" I'd trim the "View" part and the button would be like in the picture with content "Calculator".

Also I don't know how to pass the view name for each separate button for the "CommandParameter". Even passing the button content would suffice, as I can handle it from there.

How can I achieve this?

mummy
  • 49
  • 5
  • Create a class, say `ViewDefinition`, with two properties `string ViewName` and `Type ViewType`. Fill your `ViewsCollection` with them by getting all views per reflection. Bind Button Content to ViewName and CommandParameter to ViewType. Then you can create an instance of the view in your `ChangeViewCommand`, you get the required type as parameter. – Steeeve Aug 28 '21 at 23:16
  • The view should not be created in code behind.. There should instead be a set of DataTemplates for different view models that create different views. Then assign an instance of a specific selected view model to the Content property of a ContentControl. The appropriate DataTemplate and hence the appropriate view would be chosen automatically. See [Data Templating Overview](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/data-templating-overview?view=netframeworkdesktop-4.8). – Clemens Aug 29 '21 at 07:13
  • Does this answer your question? [C# WPF Navigation Between Pages (Views)](https://stackoverflow.com/a/61323201/3141792) – BionicCode Aug 29 '21 at 14:43
  • @BionicCode, no because it is about a hardcoded amount of views. I need mine to dynamically populate. – mummy Aug 29 '21 at 18:48
  • It's fully dynamic. You just add models to a pool and select from this pool. The view will be automatically rendered based on the appropriate DataTemplate. It can't get more dynamic. – BionicCode Aug 29 '21 at 19:03
  • You can replace the StackPanel that hosts the navigation buttons with a ListBox if you want to add buttons dynamically. – BionicCode Aug 29 '21 at 19:05
  • @BionicCode, oh you mean because of the implementation in the MainWindow viewmodel? Yes that would partially answer the question. However I am also asking how to populate the collection and apply the DataTemplate in such a way that it would be able to handle a collection like that (mainly how to bind the buttons to the main page's command, yet their content and command parameter to the specific view's name? So, it's only 1/3 of an answer for my question. – mummy Aug 29 '21 at 19:26
  • I will post an answer that is based on the provided link, as this is the pattern you should chose. The only adjustment I make is that the navigation buttons will be dynamic too. – BionicCode Aug 29 '21 at 19:40
  • We do this using Prism. https://prismlibrary.com/docs/wpf/introduction.html – Robert Harvey Aug 29 '21 at 19:57

2 Answers2

1

My answer is based on this answer which already shows the recommended pattern to display dynamic views. I only modified the MainViewModel logic to handle a collection of button items NavigationItem as source collection for a ListBox in the view that dynamically shows the actual navigational buttons.
The mapping will be by the index of the page model items - but it can be any other identifier.

The pattern is simple: create a model, add it to the view by binding it to a ListBox (NavigationItem model collection) and ContentControl (selected IPage or object model) and let WPF render the appropriate view using a matching implicit DataTemplate.

Replace the MainViewModel.cs and the MainWindow.xaml of the linked answer with this ones.

MainViewModel.cs
The part that dynamically adds new pages and a their corresponding navigation button is showed simplified in the AddPage() method. Basically the pattern is to define a NavigationItem and the corresponding IPage implementation. The mapping is NavigationItem.PageIndex to the corresponding item at this index of the Pages collection:

IPage nextPageModel = Pages[NavigationItem.PageIndex];

You would have to define a new DataTemplate for each added page model type to define the actual content of the page.

class MainViewModel
{
  public ICommand SelectPageCommand => new RelayCommand(SelectPage); 
  public ObservableCollection<NavigationItem> NavigationItems { get; }   
  private Dictionary<int, IPage> Pages { get; }

  private IPage selectedPage;   
  public IPage SelectedPage
  {
    get => this.selectedPage;
    set 
    { 
      this.selectedPage = value; 
      OnPropertyChanged();
    }
  }

  public MainViewModel()
  {
    this.NavigationItems = new ObservableCollection<NavigationItem>
    {
      new NavigationItem("Home", 0, this.SelectPageCommand),
      new NavigationItem("Login", 1, this.SelectPageCommand)
    };
     
    this.Pages = new Dictionary<int, IPage>
    {
      { 0, new WelcomePageViewModel() },
      { 1, new LoginPageViewModel() }
    };

    this.SelectedPage = this.Pages.First().Value;
  }

  public void SelectPage(object param)
  {
    if (param is int pageIndex 
      && this.Pages.TryGetValue(pageIndex, out IPage selectedPage))
    {
      this.SelectedPage = selectedPage;
    }
  }

  public void AddPage()
  {
    int newPageIndex = this.Pages.Count;

    IPage calculatorPageModel = new CalculatotPageViewModel();
    this.Pages.Add(newPageIndex, calculatorPageModel);

    var navigationItem = new NavigationItem("Calculator", newPageIndex, this.SelectPageCommand);
    this.NavigationItems.Add(navigationItem);
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 
    => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

NavigationItem.cs

class NavigationItem
{
  public string PageTitle { get; }  
  public string PageIndex { get; }
  public string NavigateCommand { get; }


  public NavigationItem(string pageTitle, int pageIndex, ICommand navigateCommand)
  {
    this.PageTitle = pageTitle;
    this.PageIndex = pageIndex;
    this.NavigateCommand = navigateCommand;
  }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type WelcomePageviewModel}">
      <WelcomPage />
    </DataTemplate>

    <DataTemplate DataType="{x:Type LoginPageviewModel}">
      <LoginPage />
    </DataTemplate>

    <DataTemplate DataType="{x:Type CalculatorPageviewModel}">
      <CalculatorPage />
    </DataTemplate>
  </Window.Resources>

  <StackPanel>

    <!-- Page navigation -->
    <ListBox ItemsSource="{Binding NavigationItems}">
      <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
          <VirtualizingStackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
      </ListBox.ItemsPanel>

      <ListBox.ItemTemplate> 
        <DataTemplate> 
          <Button Content="{Binding PageTitle}" 
                  Command="{Binding NavigateCommand}" 
                  CommandParameter="{Binding PageIndex}" />
        </DataTemplate> 
      </ListBox.ItemTemplate> 
    </ListBox>

    <!-- 
      Host of SelectedPage. 
      Automatically displays the DataTemplate that matches the current data type 
    -->
    <ContentControl Content="{Binding SelectedPage}" />
  <StackPanel>
</Window>
BionicCode
  • 1
  • 4
  • 28
  • 44
0

I think this is what you are after..

public class MainVM : INotifyPropertyChanged
{
    public List<ButtonViewModel> Navigation { get; private set; }

    private object _selectedView;

    public object SelectedView
    {
        get { return _selectedView; }
        set
        {
            _selectedView = value;
            OnPropertyChanged();
        }
    }

    public ICommand ChangeViewCommand { get; }


    public MainVM()
    {
        Navigation = new List<ButtonViewModel>
        {
            new ButtonViewModel
            {
                Content = "Button 1",
                CommandParameter = new VM1()
            },
            new ButtonViewModel
            {
                Content = "Button 2",
                CommandParameter = new VM2()
            }
        };

        ChangeViewCommand = new RelayCommand(param => ChangeView(param));
    }

    void ChangeView(object param)
    {
        SelectedView = param;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

public class VM1
{

}
public class VM2
{

}

public class ButtonViewModel
{
    public string Content { get; set; }
    public object CommandParameter { get; set; }
}
public class RelayCommand : ICommand
{
    private Action<object> execute;
    private Func<object, bool> canExecute;

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return this.canExecute == null || this.canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        this.execute(parameter);
    }
}

Main Window

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <ItemsControl ItemsSource="{Binding Navigation}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Button Content="{Binding Content}" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ChangeViewCommand}"  CommandParameter="{Binding CommandParameter}" HorizontalAlignment="Center" VerticalAlignment="Center" Height="40"/>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    <ContentPresenter Content="{Binding SelectedView}" Grid.Column="1" />
    
</Grid>

VM1View

<Grid>
    <TextBlock Text="I am VM1" />
</Grid>

VM2View

<Grid>
    <TextBlock Text="I am VM2" />
</Grid>

App.xaml

<Application x:Class="WpfApp.App"...>
    <Application.Resources>
        <DataTemplate DataType="{x:Type local:VM1}">
            <local:VM1View />
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:VM2}">
            <local:VM2View />
        </DataTemplate>
    </Application.Resources>
</Application>

Demo

enter image description here

Eduards
  • 1,734
  • 2
  • 12
  • 37