Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM: View Navigation not working correctly

Tags:

c#

mvvm

wpf

xaml

I used Brian Noyes's Pluralsight course, "WPF MVVM In Depth" as my main source, and what he shows works excellently.

However, instead of switching Views based on buttons clicked on the UtilitiesView, I want to switch Views based on a Toolbar button (that forms part of a VS 2015 extension package) where the user can choose a specific instance.

The UtilitiesView is a user control on the window that is opened by the Package Extension. So here is the xaml in the UtilitiesView:`

<UserControl.Resources>
    <DataTemplate DataType="{x:Type engines:CalcEngineViewModel}">
        <engines:CalcEngineView/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type engines:TAEngineViewModel}">
        <engines:TAEngineView/>
    </DataTemplate>
</UserControl.Resources>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
        <RowDefinition Height="*" />                         
    </Grid.RowDefinitions>
    <Grid x:Name="NavContent">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width ="*"/>
            <ColumnDefinition Width ="*"/>
            <ColumnDefinition Width ="*"/>
        </Grid.ColumnDefinitions>
        <Button Content="Calc"
                Command ="{Binding ChangeViewModelCommand}"
                CommandParameter="CalculationEngine"
                Grid.Column="0"/>
        <Button Content="TA"
                Command ="{Binding ChangeViewModelCommand}"
                CommandParameter="TAEngine"
                Grid.Column="1"/>

    </Grid>
    <Grid x:Name="MainContent"
        Grid.Row="1">
        <ContentControl Content="{Binding CurrentEngineViewModel}"/>
    </Grid>

</Grid>
</UserControl>`

As can be seen, there are two buttons that switch the View by binding to ChangeViewModelCommand and passing a string value (either "CalculationEngine" or "TAEngine") through.

Here is the UtilitiesViewModel.cs class:

 public class UtilitiesViewModel : BindableBase
{
    #region Fields

    public RelayCommand<string> ChangeViewModelCommand { get; private set; }

    private CalcEngineViewModel calcViewModel = new CalcEngineViewModel();
    private TAEngineViewModel taViewModel = new TAEngineViewModel(); 

    private BindableBase currentEngineViewModel;

    public BindableBase CurrentEngineViewModel
    {
        get { return currentEngineViewModel; }
        set
        {
            SetProperty(ref currentEngineViewModel, value);
        }
    }


    #endregion

    public UtilitiesViewModel()
    {          
        ChangeViewModelCommand = new RelayCommand<string>(ChangeViewModel);      
    }



    #region Methods

    public void ChangeViewModel(string viewToShow) //(IEngineViewModel viewModel)
    {
        switch (viewToShow)
        {
            case "CalculationEngine":
                CurrentEngineViewModel = calcViewModel;
                break;
            case "TAEngine":
                CurrentEngineViewModel = taViewModel;
                break;
            default: 
                CurrentEngineViewModel = calcViewModel;
                break;
        }            
    }

    #endregion
}

Here is BindableBase.cs:

public class BindableBase : INotifyPropertyChanged
{
    protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(member, val)) return;

        member = val;
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged = delegate { };
    protected virtual void OnPropertyChanged(string propertyName)
    {         
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));     
    }
}

I use a simple ViewModelLocator class to link the Views with their ViewModels:

 public static class ViewModelLocator
{
    public static bool GetAutoWireViewModel(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoWireViewModelProperty);
    }

    public static void SetAutoWireViewModel(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoWireViewModelProperty, value);
    }

    // Using a DependencyProperty as the backing store for AutoWireViewModel.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AutoWireViewModelProperty =
        DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false, AutoWireViewModelChanged));

    private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (DesignerProperties.GetIsInDesignMode(d)) return;
        var viewType = d.GetType();
        var viewTypeName = viewType.FullName;
        var viewModelTypeName = viewTypeName + "Model";
        var viewModelType = Type.GetType(viewModelTypeName);
        var viewModel = Activator.CreateInstance(viewModelType);
        ((FrameworkElement)d).DataContext = viewModel;
    }
}

As mentioned earlier, switching Views with the buttons defined on UtilitiesView.xaml works fine.

The Toolbar buttons call the above-mentioned ChangeViewModel method in UtilitiesViewModel.cs from out of the Package.cs class, but then even though the CurrentEngineViewModel property is set differently, it doesn't reflect on UtilitiesView.xaml.

When I debug, then with both cases it goes correctly to the SetProperty of BindableBase, but then in the case of the ToolBar buttons, the AutoWireViewModelChanged method in the ViewModelLocator is never called.

I don't know why not. I would have thought that the binding in UtilitiesView with the property CurrentEngineViewModel of UtilitiesViewModel, would be enough? I try to think of it as if I have made a change in the model-component, and the View should respond to that, even though I actually have the Toolbar buttons as part of what one would consider the view-component.

This is how the ChangeViewModel method is called in the Package.cs class:

if (Config.Engine.AssemblyPath.Contains("Engines.TimeAndAttendance.dll"))
                {
                    uvm.ChangeViewModel("TAEngine");
                }
                else //Assume Calculation Engine
                {
                    uvm.ChangeViewModel("CalculationEngine"); 
                }

I hope I have given enough detail.

UPDATE 1

With regards to gRex's comments, I am thinking that perhaps there are two UtilitiesViewModel objects.

This is what happens when the custom window of the Package Extension is opened:

public class SymCalculationUtilitiesWindow : ToolWindowPane
{
    /// <summary>
    /// Initializes a new instance of the <see cref="SymCalculationUtilitiesWindow"/> class.
    /// </summary>
    public SymCalculationUtilitiesWindow() : base(null)
    {
        this.Caption = "Sym Calculation Utilities";

        this.ToolBar = new CommandID(new Guid(Guids.guidConnectCommandPackageCmdSet), Guids.SymToolbar);
        // This is the user control hosted by the tool window; Note that, even if this class implements IDisposable,
        // we are not calling Dispose on this object. This is because ToolWindowPane calls Dispose on
        // the object returned by the Content property.
        this.Content = new UtilitiesView();

    }

}

The AutoWireViewModelChanged method is called to link the UtilitiesViewModel to the UtilitiesView for the Content.

In the Package.cs class, I have this field:

 private UtilitiesViewModel uvm;

and in the Initialize method I have:

 uvm = new UtilitiesViewModel();

The uvm object is used as in the code snippet in the original post (just above the UPDATE) to call the ChangeViewModel method with the appropriate string parameter.

This would give me two different objects, wouldn't it? If so, and assuming that this could be the root cause of the problem, how can I improve this, must I make UtilitiesViewModel a singleton?

UPDATE 2

I have added a solution to Github. The functionality is slightly changed so that I didn't need any interaction with the rest of the original solution. Thus, the Connect button (on the Toolbar) calls the ChangeViewModel method with the "TAEngine" parameter, and the Save button (on the Toolbar) does the same but with "CalculationEngine" as parameter. Currently the DataTemplates are still commented out, so one just sees the class name in text. Here is the link. In the experimental instance of Visual Studio, the window can be found at View -> Other Windows -> SymplexityCalculationUtilitiesWindow. You might need to download the Visual Studio SDK if you don't have it already.

Update 3

I used the Unity IoC Container with a ContainerControlledLifetimeManager to ensure that I don't have two distinct UtilitiesViewModels. After implementing this, the Toolbar buttons could navigate the correct View.

like image 311
Igavshne Avatar asked Dec 12 '15 14:12

Igavshne


1 Answers

If there is no Binding Error, check if the uvm Object is set for the DataContext of the view.

You can see the changes in the DataContext Tab with snoop

  1. First drag and drop the crosshair to the window.
  2. Select one control by pressing Strg+Shift and Mouse over
  3. Switch to datacontext-Tab and see, if CurrentEngineViewModel changed.

[Update] Based on your comment I assume, the uvm Object used by the ToolBar-Buttons is not the one, which is set to the DataContext of your View. So the Changes can not take Effect.

Please check the code, where you get the uvm Object and the Initialisation of the DataContext.

[Update2] You have to solve the "you have two objects" Problem. Making the ViewModel a singelton would serve. I would prefer to introduce some kind of bootstrapping and/or a singelton service to access the viewmodels.

And then instead of

uvm = new UtilitiesViewModel();

you can set it like:

uvm = yourService.GetUtilitiesViewModel();

with factory or cache. If you use the same object your datatemplates will work immediately.

[++] MVVM has a hard learning curve at the beginning, because of so many different ways you can do it. But believe me, the benefit is worth the effort. Here some links

  • recommendations-best-practices
  • viewmodel-locator-for-mvvm

but I'm not sure, if this fits to the Brian Noyes's Pluralsight course, your viewm-model locator and your specific bootstrapping.

For additional help, here something that came in my mind, based on the information you provided in this post. The missing link to register your ViewModel in your Service could be done in a Command triggerd by the loaded Event of your view:

In your view you could Call a Command to register your ViewModel:

<Window ... >
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <core:EventToCommand Command="{Binding RegisterViewModelCommand}" PassEventArgsToCommand="False"/>
    </i:EventTrigger>
</i:Interaction.Triggers>
<Grid>

by referencing System.Windows.Interactivity.dll from Expression blend and some Implementations from EventToCommand like in MvvmLight.

Then in your Command-Handler you call yourService.RegisterUtilitiesViewModel(this)

Not quite sure if this is the best approach, but at least, it is one. I would prefer to do some Bootstrapping with Prism and Dependency-Injection, but this is another story.

like image 82
gReX Avatar answered Nov 20 '22 07:11

gReX