简体   繁体   中英

How to bind 2 commands from 2 different ViewModels to a Button. WPF MVVM

I need a button to execute two commands from two different ViewModels. I am using communitytoolkit.mvvm

Lets make an example:

INavigator:

public enum ViewType
{
    MainView,
    ItemView,
    AddNewItemView
}

public interface INavigator
{
      BaseVM CurrentViewModel { get; set; }
}

Navigator:

[INotifyPropertyChanged]
public partial class Navigator : INavigator
{
   [ObservableProperty]
   BaseVM currentViewModel = new DashboardVM();

   [ICommand]
   public void UpdateViewModel(object parameter)
   {
       if (parameter is ViewType)
       {
           ViewType viewType = (ViewType)parameter;

           switch (viewType)
           {
               case ViewType.MainView:
                   CurrentViewModel = new MainViewModel();
                   break;
               case ViewType.ItemView:
                   CurrentViewModel = new ItemViewModel();
                   break;
               case ViewType.AddNewItemView:
                   CurrentViewModel = new AddNewItemViewModel();
                   break;
               default:
                   break;
           }
        }
    }
}

MainView:

xmlns:nav="clr-namespace:........."

   <StackPanel DataContext="{Binding Navigator}">

        <Button Command="{Binding UpdateViewModelCommand}"
                CommandParameter="{x:Static nav:ViewType.MainView}"/>

        <Button Command="{Binding UpdateViewModelCommand}"
                CommandParameter="{x:Static nav:ViewType.ItemView}"/>

        <Button Command="{Binding UpdateViewModelCommand}"
                CommandParameter="{x:Static nav:ViewType.AddNewItemView}"/>

   </StackPanel>

  <ContentControl DataContext="{Binding Navigator}"
                  Content="{Binding CurrentViewModel}"/>

AddNewItemView:

   <.....>
   

        <Button Command="{Binding AddNewItemCommand}"/>       

   <.....>

AddNewItemViewModel:

[INotifyPropertyChanged]
public partial class ItemViewModel
{
    [ICommand]
    public void AddNewItem()
    {
        //Add new Item
    }
}

MainViewModel:

[INotifyPropertyChanged]
public partial class MainViewModel
{
     public INavigator Navigator { get; set; } = new Navigator();
}

I want to Bind:

Command="{Binding UpdateViewModelCommand}" CommandParameter="{x:Static nav:ViewType.ItemView}"

To the Button in the AddItemView, basically I want that Button to execute the "AddNewItemCommand" and then "UpdateViewModelCommand" to go back to ItemView.

Maybe my approach is wrong, or is there an easier way to navigate?

Microsoft.Xaml.Behaviors.Wpf allows you to invoke multiple Commands by one event.

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction Command="{Binding AddNewItemCommand, ElementName=...}"/>
            <i:InvokeCommandAction Command="{Binding UpdateViewModelCommand, ElementName=...}"
                                   CommandParameter="{x:Static nav:ViewType.ItemView}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

You must improve your view model class design.

Your navigation model is controlled by the View Model . This means the general navigation logic takes place in the View Model . The View only triggers the navigation.
Therefore, the navigation back to a particular view after the current view's View Model class has completed its operation, must be executed by the View Model too.

Moving the navigation logic to a dedicated type, like your created INavigator , to encapsulate the logic and to enable sharing, is the first step.

Next step is to share the same instance of INavigator between all View Model classes that participate in the navigation logic.

You can further improve the design by adding a INavigator.NavigateToPrevious method to the INavigator .
This way the view model class must not know any particular ViewType if its only goal is to navigate back to the previous view model.

To implement such a feature, INavigator would have to maintain a navigation history eg, by using a Stack<ViewType> as backing store, to navigate backwards.
To do so, simply push the current ViewType of the CurrentViewModel to the Stack<ViewType> and pop it later in your NavigateToPrevious method:

INavigator.cs

interface INavigator
{
  // The set() of this property must be private to add robustness
  BaseVM CurrentViewModel { get; }

  NavigateToViewModelCommand { get; }

  void NavigateTo(ViewType nextViewType);
  bool NavigateToPrevious();
}

Navigator.cs
Note how the example uses a Dictionary that returns a view model factory to avoid the implementation of a type switch.
Also note how using a Stack<ViewType> enables a navigation history.

class Navigator : INavigator
{
  [ObservableProperty]
  BaseVM currentViewModel = new DashboardVM();
  
  private Dictionary<ViewType, Func<INavigator, BaseVM>> ViewModelFactoryMap { get; }
  private Stack<ViewType> ViewHistory { get; }
  private ViewType CurrentViewType { get; set; }

  public Navigator()
  {
    this.ViewHistory = new Stack<ViewType>();
    this.ViewModelFactoryMap = new Dictionary<ViewType, Func<INavigator, BaseVM>>
    {
      { ViewType.ItemView, navigator => new ItemViewModel(navigator) },
      { ViewType.MainView, navigator => new MainViewModel(navigator) },
      { ViewType.AddNewItemView, navigator => new AddNewItemViewModel(navigator) },
    };

    // Load initial view
    NavigateTo(ViewType.MainView);
  }

  public void NavigateTo(ViewType nextViewType)
  {
    if (this.ViewModelFactoryMap.TryGetValue(nextViewType, out Func<INavigator, BaseVM> viewModelFactory))
    {
      // Maintain a navigation history
      this.ViewHistory.Push(this.CurrentViewType);

      this.CurrentViewModel = viewModelFactory.Invoke(this);
      this.CurrentViewType = nextViewType;
    }
  }

  public bool NavigateToPrevious()
  {
    if (this.ViewHistory.TryPop(out Viewtype previousViewType))
    {
      NavigateTo(previousViewType);
      return true;
    }
   
    return false;
  }

  [ICommand]
  public void NavigateToViewModelCommand(object commandParameter)
  {
    if (commandParameter is ViewType viewType)
    {
      NavigateTo(viewtype);
    }
  }
}

AddNewItemViewModel.cs
Because AddNewItemViewModel must know how to navigate, its constructor must accept a shared INavigator instance. Follow this pattern for all View Model classes that must know how to navigate.
Since INavigator exposes a INavigator.NavigateToPrevious method, View Model classes can navigate to the previous view anonymously:

class AddNewItemViewModel : BaseVM
{ 
  public ICommand AddNewItemCommand => new DelegateCommand(ExecuteAddNewItemCommand);
  private INavigator Navigator { get; }

  public AddNewItemViewModel(INavigator navigator)
  {
    this.Navigator = navigator;
  }

  private void ExecuteAddNewItemCommand(object commandParameter)
  {
    // TODO::Execute command

    this.Navigator.NavigateToPrevious();
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM