MVVM: просмотр навигации работает неправильно
Я использовал курс Brian Noyes Pluralsight, "WPF MVVM In Depth" в качестве основного источника, и то, что он показывает, отлично работает.
Однако вместо переключения Views на основе кнопок, нажимаемых на UtilitiesView, я хочу переключать Views на основе кнопки панели инструментов (которая является частью пакета расширения VS 2015), где пользователь может выбрать конкретный экземпляр.
UtilitiesView - это пользовательский элемент управления в окне, которое открывается расширением пакета.
Итак, вот xaml в 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>`
Как можно видеть, есть две кнопки, которые переключают представление путем привязки к ChangeViewModelCommand и передают строковое значение (либо "CalculationEngine", либо "TAEngine" ) через.
Вот класс UtilitiesViewModel.cs:
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
}
Вот 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));
}
}
Я использую простой класс ViewModelLocator, чтобы связать Views с их 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;
}
}
Как упоминалось ранее, переключать представления с помощью кнопок, определенных в UtilitiesView.xaml, отлично.
Кнопки панели инструментов вызывают вышеупомянутый метод ChangeViewModel в UtilitiesViewModel.cs из класса Package.cs, но даже если свойство CurrentEngineViewModel задано по-разному, оно не отражается на UtilitiesView.xaml.
Когда я отлаживаю, то в обоих случаях он корректно подходит к SetProperty для BindableBase, но тогда в случае кнопок ToolBar метод AutoWireViewModelChanged в ViewModelLocator никогда не вызывается.
Я не знаю, почему нет. Я бы подумал, что привязки в UtilitiesView с свойством CurrentEngineViewModel из UtilitiesViewModel будет достаточно?
Я пытаюсь думать об этом, как если бы я внес изменения в модель-компонент, и View должен ответить на это, даже если у меня на самом деле есть кнопки панели инструментов в качестве части того, что можно было бы рассмотреть компонент вида.
Так вызывается метод ChangeViewModel в классе Package.cs:
if (Config.Engine.AssemblyPath.Contains("Engines.TimeAndAttendance.dll"))
{
uvm.ChangeViewModel("TAEngine");
}
else //Assume Calculation Engine
{
uvm.ChangeViewModel("CalculationEngine");
}
Надеюсь, я дал достаточно подробностей.
ОБНОВЛЕНИЕ 1
Что касается комментариев gRex, я думаю, что, возможно, есть два объекта UtilitiesViewModel.
Это то, что происходит, когда открывается пользовательское окно расширения пакета:
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();
}
}
Метод AutoWireViewModelChanged вызывается для связывания утилиты UtilitiesViewModel с UtilitiesView для содержимого.
В классе Package.cs у меня есть это поле:
private UtilitiesViewModel uvm;
а в методе Initialize у меня есть:
uvm = new UtilitiesViewModel();
Объект uvm используется как в фрагменте кода в исходном сообщении (чуть выше UPDATE), чтобы вызвать метод ChangeViewModel с соответствующим строковым параметром.
Это даст мне два разных объекта, не так ли?
Если да, и если предположить, что это может быть основной причиной проблемы, как я могу ее улучшить, должен ли я сделать UtilitiesViewModel синглом?
ОБНОВЛЕНИЕ 2
Я добавил решение для Github. Функциональность немного изменена, так что мне не нужно было взаимодействовать с остальной частью исходного решения.
Таким образом, кнопка Connect (на панели инструментов) вызывает метод ChangeViewModel с параметром "TAEngine", а кнопка "Сохранить" (на панели инструментов) делает то же самое, но с параметром "CalculationEngine". В настоящее время DataTemplates по-прежнему закомментированы, поэтому вы просто видите имя класса в тексте.
Вот ссылка . В экспериментальном экземпляре Visual Studio это окно можно найти в меню Вид → Другие Windows → SymplexityCalculationUtilitiesWindow.
Возможно, вам потребуется загрузить SDK Visual Studio, если у вас его еще нет.
Обновление 3
Я использовал контейнер Unity IoC ContainerControlledLifetimeManager, чтобы убедиться, что у меня нет двух разных UtilitiesViewModels. После выполнения этой функции кнопки панели инструментов могут перемещаться по правильному представлению.
Ответы
Ответ 1
Если нет ошибки привязки, проверьте, установлен ли объект uvm для DataContext представления.
Вы можете увидеть изменения в вкладке DataContext с помощью snoop
- Сначала перетащите перекрестье в окно.
- Выберите один элемент управления, нажав Strg + Shift и Mouse over
- Перейдите на вкладку datacontext-Tab и посмотрите, изменилось ли CurrentEngineViewModel.
[Обновление]
Исходя из вашего комментария, я предполагаю, что объект uvm, используемый кнопками ToolBar, не тот, который установлен в DataContext вашего представления. Таким образом, изменения не могут повлиять.
Пожалуйста, проверьте код, где вы получаете объект uvm и инициализацию DataContext.
[Обновление2]
Вы должны решить проблему "у вас есть два объекта". Создание ViewModel a singelton будет служить. Я бы предпочел ввести какой-то загрузочный и/или сервисы singelton, чтобы получить доступ к режимам просмотра.
А затем вместо
uvm = new UtilitiesViewModel();
вы можете установить его так:
uvm = yourService.GetUtilitiesViewModel();
с factory или кешем. Если вы используете тот же объект, ваши данные будут работать немедленно.
[++]
MVVM имеет сложную кривую обучения в начале, из-за так много разных способов, которыми вы можете это сделать. Но поверьте мне, польза стоит усилий.
Здесь некоторые ссылки
но я не уверен, если это соответствует курсу Brian Noyes Pluralsight, вашему локатору модели viewm и вашей конкретной загрузке.
Для дополнительной помощи, здесь что-то, что пришло мне в голову, на основе информации, предоставленной вами в этом сообщении. Отсутствующая ссылка для регистрации вашего ViewModel в вашей службе может быть выполнена в триггере Command загруженным событием вашего представления:
В вашем представлении вы можете вызвать команду, чтобы зарегистрировать ViewModel:
<Window ... >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<core:EventToCommand Command="{Binding RegisterViewModelCommand}" PassEventArgsToCommand="False"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
путем ссылки System.Windows.Interactivity.dll из сочетания Expression и некоторых реализаций из EventToCommand, как в MvvmLight.
Затем в вашем Command-Handler вы вызываете
yourService.RegisterUtilitiesViewModel(this)
Не совсем уверен, что это лучший подход, но, по крайней мере, он один. Я бы предпочел сделать некоторый бутстрапинг с Prism и Dependency-Injection, но это еще одна история.