MVVM Navigating through different Views

2019-09-09 15:46发布

问题:

I've spent the last days reading and trying to apply the Navigation pattern from this page: https://rachel53461.wordpress.com/2011/12/18/navigation-with-mvvm-2/

Now, after I got my project to work I'm really confused about how the binding works here. At first I have to clarify that I don't want a Navigation pane which is always visible like in the given example. I just want to use my MainView for navigation and each "SubView" should be able to go back to it's "parent" only.

Here's what I've got:

Project: APP

Class: App.xaml.cs

protected override void OnStartup(StartupEventArgs e) {
        base.OnStartup(e);
        UI.View.Main.MainView app = new UI.View.Main.MainView();
        UI.View.Main.MainViewModel viewModel = new UI.View.Main.MainViewModel(some dependencies);
        app.DataContext = viewModel;
        app.Show();
    }

ViewModel Base Class

public abstract class BaseViewModel : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    private string _name;
    public string Name {
        get {
            return _name;
        }
        set {
            if (Name != value) {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
    }
    private BaseViewModel _homePage;
    public BaseViewModel HomePage {
        get {
            return _homePage;
        }
        set {
            if (HomePage != value) {
                _homePage = value;
                OnPropertyChanged("HomePage");
            }
        }
    }
    public void OnPropertyChanged(string propertyName) {
        PropertyChangedEventHandler temp = PropertyChanged;
        if (temp != null) {
            temp(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}

MainViewModel

namespace SGDB.UI.View.Main {
    public class MainViewModel : BaseViewModel {


private BaseViewModel _currentPageViewModel;
        public BaseViewModel CurrentPageViewModel {
            get {
                return _currentPageViewModel;
            }
            set {
                if (CurrentPageViewModel != value) {
                    _currentPageViewModel = value;
                    OnPropertyChanged("CurrentPageViewModel");
                }
            }
        }
        public List<BaseViewModel> PageViewModels { get; private set; }
        public RelayCommand ChangePageCommand {
            get {
                return new RelayCommand(p => ChangeViewModel((BaseViewModel)p), p => p is BaseViewModel);
            }
        }

        //Some Dependencies

        public List<BaseViewModel> ViewPages { get; private set; }

        public MainViewModel(some dependencies) {
            HomePage = new HomeViewModel() { Name = "TEST" };
            //assign dependencies
            var uavm = new UserAdministration.UserAdministrationViewModel(_userUnitOfWork, _personUnitOfWork) {
                Name = Resources.Language.Sys.UserAdministartionTitle
            };
            PageViewModels = new List<BaseViewModel>();
            PageViewModels.Add(uavm);
            ChangeViewModel(HomePage);
        }


        public void ChangeViewModel(BaseViewModel viewModel) {
            CurrentPageViewModel = viewModel;
        }
    }
}

MainView

<Window x:Class="SGDB.UI.View.Main.MainView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:SGDB.UI.View.Main"
    xmlns:ua="clr-namespace:SGDB.UI.View.UserAdministration"
    xmlns:home="clr-namespace:SGDB.UI.View.Home"
    mc:Ignorable="d"
    Title="MainView" Height="400" Width="800">
<Window.Resources>
    <DataTemplate DataType="{x:Type home:HomeViewModel}">
        <home:Home/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type ua:UserAdministrationViewModel}">
        <ua:UserAdministration/>
    </DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentPageViewModel}"/>

HomeViewModel

public class HomeViewModel : BaseViewModel {
    public RelayCommand TestCommand {
        get {
            return new RelayCommand((x) => MessageBox.Show(x.ToString()), (x) => true);
        }
    }
}

HomeView

<UserControl x:Class="SGDB.UI.View.Home.Home"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:SGDB.UI.View.Home"
    xmlns:controls="clr-namespace:SGDB.UI.Controls"
    xmlns:resx="clr-namespace:SGDB.UI.Resources.Language"
    mc:Ignorable="d"
    d:DesignHeight="400" d:DesignWidth="800">
<Grid>
    <Grid.Resources>
        <Style TargetType="controls:ModernButton">
            <Setter Property="Margin" Value="1"/>
            <Setter Property="FontFamily" Value="Bosch Office Sans"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Size" Value="155"/>
        </Style>
    </Grid.Resources>
    <Grid.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
            <LinearGradientBrush.GradientStops>
                <GradientStop Color="#26688B" Offset="1"/>
                <GradientStop Color="#11354C" Offset="0"/>
            </LinearGradientBrush.GradientStops>
        </LinearGradientBrush>
    </Grid.Background>
    <Grid.RowDefinitions>
        <RowDefinition Height="60"/>
        <RowDefinition Height="3*"/>
        <RowDefinition Height="2*"/>
        <RowDefinition Height="25"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="150"/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center">
        <StackPanel.Resources>
            <Style TargetType="TextBlock">
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="FontFamily" Value="Bosch Office Sans"/>
            </Style>
        </StackPanel.Resources>
        <TextBlock Text="{x:Static resx:Sys.ApplicationTitle}"  FontSize="20" FontWeight="Bold" Margin="5"/>
        <TextBlock Text="{x:Static resx:Sys.ApplicationSubTitle}"  FontSize="12" FontWeight="Light"/>
    </StackPanel>
    <WrapPanel Grid.Row="1" 
               Grid.Column="0" 
               FlowDirection="LeftToRight" 
               HorizontalAlignment="Left" 
               Width="367">
        <ItemsControl ItemsSource="{Binding DataContext.PageViewModels, RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <controls:ModernButton Background="Dark" 
                               Text="{Binding Name}"
                                       Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
                                       CommandParameter="{Binding}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
            CommandParameter="{Binding HomePage}"/>
// This Button is always disabled although HomePage is of Type HomeViewModel which is based on BaseViewModel.
    </WrapPanel>
</Grid>

My questions are:

  1. Why does the HomeView knot that the HomeViewModel is it's ViewModel? I do not define it anywhere in my code.
  2. Why does the Binding on the Name Property work but binding to the HomePage Property doesn't? Both of them are defined in the BaseViewModel class.

Update 1:

RelayCommand class:

public class RelayCommand : ICommand {
    public event EventHandler CanExecuteChanged;

    readonly Action<object> _action;
    readonly Predicate<object> _predicate;
    public RelayCommand(Action<object> action, Predicate<object> predicate) {
        _action = action;
        _predicate = predicate;
    }
    public RelayCommand(Action<object> action) {
        _action = action;
        _predicate = ((x) => true);
    }

    public bool CanExecute(object parameter) {
        return _predicate(parameter);
    }

    public void Execute(object parameter) {
        _action(parameter);
    }
}

Update 2:

What's the actual problem?

<Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
            CommandParameter="{Binding HomePage}"/>

The Content gets bound properly but the CommandParameter (HomePage) which should be of Type BaseViewModel won't get validated through the Command's CanExecute. Both the Properties, Name and HomePage are defined inside the BaseViewModel.

Update 3:

<Button Content="{Binding Name}" Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
        CommandParameter="{Binding DataContext.HomePage, ElementName=Test}"/>

回答1:

  1. In your there is the next lines:

    <DataTemplate DataType="{x:Type home:HomeViewModel}">
            <home:Home/>
    </DataTemplate> 
    

meaning that the visual form of HomeViewModel is Home.

  1. Your Binding works fine, I think your problem is the command itself. I don't know what is RelayCommand but i think your bug is from there. RelayCommand should be something like this:

    public abstract class BaseViewModel : INotifyPropertyChanged 
    {
        private ICommand _f1KeyCommand;
    
        public ICommand F1KeyCommand
        {
            get
            {
                if (_f1KeyCommand == null)
                    _f1KeyCommand = new DelegateCommand(F1KeyCommandCallback, CanExecute);
    
                return _f1KeyCommand;
            }
        }
    
        /// <summary>
        /// Fired if F1 is pressend and 'CanExecute' returns true
        /// </summary>
        private void F1KeyCommandCallback(object obj)
        {
            Console.WriteLine("F1KeyCommandCallback fired");
        }
    
        // ....
    }
    

This class allows delegating the commanding logic to methods passed as parameters,and enables a View to bind commands to objects that are not part of the element tree:

public class DelegateCommand : ICommand
{
    #region Data Members

    private Action<object> execute;
    private Predicate<object> canExecute;
    private event EventHandler CanExecuteChangedInternal;

    #endregion

    #region Ctor

    public DelegateCommand(Action<object> execute)
       : this(execute, DefaultCanExecute)
    {
    }

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
       if (execute == null)
       {
           throw new ArgumentNullException("execute");
       }

       if (canExecute == null)
       {
           throw new ArgumentNullException("canExecute");
       }

       this.execute = execute;
       this.canExecute = canExecute;
    }


    #endregion

    #region Properties

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            this.CanExecuteChangedInternal += value;
        }

        remove
        {
            CommandManager.RequerySuggested -= value;
            this.CanExecuteChangedInternal -= value;
        }
    }

    #endregion

    #region Public Methods

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

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

    public void OnCanExecuteChanged()
    {
        EventHandler handler = this.CanExecuteChangedInternal;
        if (handler != null)
        {
            handler.Invoke(this, EventArgs.Empty);
        }
    }

    public void Destroy()
    {
        this.canExecute = _ => false;
        this.execute = _ => { return; };
    }

    #endregion

    #region Private Methods

    private static bool DefaultCanExecute(object parameter)
    {
        return true;
    }

    #endregion
}

In your view:

 <controls:ModernButton Background="Dark" 
                       Text="{Binding Name}"
                       Command="{Binding F1KeyCommand"
                       CommandParameter="{Binding}"/>


标签: c# wpf xaml mvvm