Ho utilizzato il corso Pluralsight di Brian Noyes, "WPF MVVM In Depth" come fonte principale e ciò che mostra funziona in modo eccellente.MVVM: Visualizzazione della navigazione non funzionante correttamente
Tuttavia, invece di passare a Visualizzazioni basate sui pulsanti cliccati su UtilitiesView, voglio cambiare Viste in base a un pulsante della barra degli strumenti (che fa parte di un pacchetto di estensioni VS 2015) in cui l'utente può scegliere un'istanza specifica.
UtilitiesView è un controllo utente sulla finestra che viene aperto dall'estensione del pacchetto. Così qui è il codice XAML nella 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>`
Come si vede, ci sono due pulsanti che passare alla visualizzazione legandosi ai ChangeViewModelCommand e passando un valore stringa (o "CalculationEngine" o "TAEngine") attraverso.
Ecco la classe 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
}
Ecco 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));
}
}
Io uso una semplice classe ViewModelLocator per collegare le opinioni con i loro 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;
}
}
Come accennato in precedenza, passare a Visualizzazioni con i pulsanti definiti su UtilitiesView.xaml w bene.
I pulsanti della barra degli strumenti chiamano il metodo ChangeViewModel sopra indicato in UtilitiesViewModel.cs dalla classe Package.cs, ma anche se la proprietà CurrentEngineViewModel è impostata in modo diverso, non si riflette su UtilitiesView.xaml.
Quando eseguo il debug, in entrambi i casi si passa correttamente alla SetProperty di BindableBase, ma nel caso dei pulsanti ToolBar, il metodo AutoWireViewModelChanged in ViewModelLocator non viene mai chiamato.
Non so perché no. Avrei pensato che il binding in UtilitiesView con la proprietà CurrentEngineViewModel di UtilitiesViewModel, sarebbe stato sufficiente? Cerco di pensarlo come se avessi apportato una modifica al componente del modello e la vista dovrebbe rispondere a questo, anche se in realtà ho i pulsanti della barra degli strumenti come parte di ciò che considereremmo il componente della vista.
Questo è come il metodo ChangeViewModel viene chiamato nella classe Package.cs:
if (Config.Engine.AssemblyPath.Contains("Engines.TimeAndAttendance.dll"))
{
uvm.ChangeViewModel("TAEngine");
}
else //Assume Calculation Engine
{
uvm.ChangeViewModel("CalculationEngine");
}
Spero di aver dato abbastanza dettagli.
UPDATE 1
Per quanto riguarda i commenti di Grex, sto pensando che forse ci sono due oggetti UtilitiesViewModel.
Questo è ciò che accade quando si apre la finestra personalizzata di estensione Pacchetto:
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();
}
}
Il metodo AutoWireViewModelChanged è chiamato a collegare l'UtilitiesViewModel al UtilitiesView per il Contenuto.
Nella classe Package.cs, ho questo campo:
private UtilitiesViewModel uvm;
e nel metodo Initialize ho:
uvm = new UtilitiesViewModel();
L'oggetto UVM viene utilizzato come nel frammento di codice in post originale (appena sopra l'UPDATE) per chiamare il metodo ChangeViewModel con il parametro stringa appropriato.
Questo mi darebbe due oggetti diversi, vero? In tal caso, e supponendo che questa possa essere la causa principale del problema, come posso migliorarlo, devo rendere UtilitiesViewModel un singleton?
UPDATE 2
ho aggiunto una soluzione per Github. La funzionalità è leggermente cambiata, quindi non ho bisogno di alcuna interazione con il resto della soluzione originale. Quindi, il pulsante Connetti (sulla barra degli strumenti) chiama il metodo ChangeViewModel con il parametro "TAEngine" e il pulsante Salva (sulla barra degli strumenti) fa lo stesso ma con "CalculationEngine" come parametro. Attualmente i DataTemplates sono ancora commentati, quindi si vede solo il nome della classe nel testo. Ecco lo link. Nell'istanza sperimentale di Visual Studio, la finestra può essere trovata in Visualizza -> Altre finestre -> SymplexityCalculationUtilitiesWindow. Potrebbe essere necessario scaricare l'SDK di Visual Studio se non lo si possiede già.
Update 3
ho usato l'Unità IoC Container con un ContainerControlledLifetimeManager per garantire che non ho due UtilitiesViewModels distinti. Dopo averlo implementato, i pulsanti della barra degli strumenti potevano navigare nella vista corretta.
Dove viene utilizzato ViewModelLocator? –
Penso che ViewModelLocator venga usato quando viene chiamato il metodo InitializeComponent() di View (che si verifica nel file code-behind della View, ad esempio in TAEngineView.xaml.cs). Quando eseguo il debug ci sono voci per il codice esterno, quindi è tutto ciò che posso vedere. Non sono sicuro del motivo o del momento in cui InitializeComponent viene attivato, penso che accada con la "magia" di DataTemplate su UtilitiesView.xaml. – Igavshne
Cosa vedi nell'interfaccia utente quando fai clic sui pulsanti? Il ContentControl (dove dovrebbero apparire le macchine virtuali) rimane vuoto? Prova a commentare i DataTemplates e vedi cosa succede: ora vedi i nomi delle tue due VM classes mentre fai clic sui pulsanti? –