2015-12-12 29 views
13

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.

+0

Dove viene utilizzato ViewModelLocator? –

+0

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

+0

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? –

risposta

1

Se non esiste alcun errore di binding verifica se l'oggetto uvm è impostato per il DataContext della vista.

si possono vedere i cambiamenti nella scheda DataContext con snoop

  1. Prima di tutto trascinare e rilasciare il mirino alla finestra.
  2. Selezionare un controllo premendo Strg + Maiusc e Mouse su
  3. Passare alla scheda datacontext e vedere se CurrentEngineViewModel è cambiato.

[Update] Sulla base della sua commento presumo, l'oggetto uvm usato dai toolbar-pulsanti non è quella, che è impostato per il DataContext della vista. Quindi le modifiche non possono avere effetto.

Controllare il codice, dove si ottiene l'oggetto uvm e l'inizializzazione di DataContext.

[Update2] Devi risolvere il problema "hai due oggetti". Rendere il ViewModel un singelton servirebbe.Preferirei introdurre una sorta di bootstrapping e/o un servizio singelton per accedere ai modellidocumenti.

E allora invece di

uvm = new UtilitiesViewModel(); 

è possibile impostare le cose come:

uvm = yourService.GetUtilitiesViewModel(); 

con la fabbrica o di cache. Se si utilizza lo stesso oggetto, i datatemplates funzioneranno immediatamente.

[++] MVVM ha una curva di apprendimento difficile all'inizio, a causa di tanti modi diversi che è possibile farlo. Ma credimi, il beneficio è valsa la pena. Ecco alcuni link

ma non sono sicuro, se questo si adatta al corso Pluralsight del Brian Noyes, il vostro Viewm modello locator e il vostro bootstrap specifica.

Per ulteriore assistenza, ecco qualcosa che mi è venuto in mente, in base alle informazioni fornite in questo post. L'anello mancante per registrare il ViewModel nel vostro servizio potrebbe essere fatto in un triggerd comando dal evento Loaded della vista:

Secondo lei si potrebbe chiamare un comando per registrare il ViewModel:

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

da referenziamento System.Windows.Interactivity.dll dalla miscela Expression e alcune implementazioni da EventToCommand come in MvvmLight.

Poi, nel tuo Command-Handler si chiamano yourService.RegisterUtilitiesViewModel(this)

Non del tutto sicuro se questo è l'approccio migliore, ma almeno, è uno. Preferirei fare un bootstrap con Prism e Dependency-Injection, ma questa è un'altra storia.

+0

Grazie per il link a Snoop. Sarà utile. Se seleziono la barra degli strumenti non ha alcun DataContext significativo. Se seleziono la parte MainContent di UtilitiesView, allora posso vedere la variabile CurrentEngineViewModel come parte di DataContext. Se poi utilizzo i pulsanti della barra degli strumenti per modificare il valore (ad esempio da CalcEngineViewModel a TAEngineViewModel), non mostra che DataContext è stato aggiornato, ma mostra ancora CalcEngineViewModel (anche se sembra passare attraverso SetProperty durante il debugging normalmente). – Igavshne

+0

Grazie per l'aiuto aggiuntivo. Sto cercando di utilizzare Unity, ma ho riscontrato alcuni errori di dipendenza dell'assembly, quindi non sono ancora stato in grado di verificare se tutto funziona insieme, ma penso che il problema principale sia che avevo due oggetti. Ti meriti la bontà. :-) – Igavshne

+0

@JustiThyme Grazie per la taglia. Io uso MEF per DI e devo anche fare attenzione. – gReX

Problemi correlati