2014-09-03 14 views
6

Questa è una domanda piuttosto lunga, quindi per favore abbiate pazienza con me.Caliburn.Micro nested ViewModels best practice

Attualmente sto sviluppando un piccolo strumento per aiutarmi a tenere traccia della miriade di personaggi nelle mie storie.

Lo strumento esegue le seguenti operazioni:

  • Caricare i personaggi che sono attualmente memorizzati come JSON sul disco e li memorizza in un elenco, che viene presentato in the Shell tramite un ListBox.
  • Se l'utente apre un carattere, Shell, che è un Conductor<Screen>.Collection.OneActive, apre un nuovo CharacterViewModel, che deriva da Screen.
  • Il Character ottiene il carattere che verrà aperto tramite il sistema di messaggi IEventAggregator.
  • Il CharacterViewModel dispone inoltre di varie proprietà che sono sub ViewModels che si associano a varie viste secondarie.

E qui è il mio problema: Attualmente mi inizializzare la ViewModels sub manualmente quando il ChracterViewModel viene inizializzato. Ma questo suona strano per me e sono abbastanza sicuro che ci sia un modo migliore per farlo, ma non riesco a vedere come dovrei farlo.

ecco il codice del CharacterViewModel:

/// <summary>ViewModel for the character view.</summary> 
public class CharacterViewModel : Screen, IHandle<DataMessage<ICharacterTagsService>> 
{ 
    // -------------------------------------------------------------------------------------------------------------------- 
    // Fields 
    // ------------------------------------------------------------------------------------------------------------------- 

    /// <summary>The event aggregator.</summary> 
    private readonly IEventAggregator eventAggregator; 

    /// <summary>The character tags service.</summary> 
    private ICharacterTagsService characterTagsService; 

    // -------------------------------------------------------------------------------------------------------------------- 
    // Constructors & Destructors 
    // ------------------------------------------------------------------------------------------------------------------- 

    /// <summary>Initializes a new instance of the <see cref="CharacterViewModel"/> class.</summary> 
    public CharacterViewModel() 
    { 
     if (Execute.InDesignMode) 
     { 
      this.CharacterGeneralViewModel = new CharacterGeneralViewModel(); 

      this.CharacterMetadataViewModel = new CharacterMetadataViewModel(); 
     } 
    } 

    /// <summary>Initializes a new instance of the <see cref="CharacterViewModel"/> class.</summary> 
    /// <param name="eventAggregator">The event aggregator.</param> 
    [ImportingConstructor] 
    public CharacterViewModel(IEventAggregator eventAggregator) 
     : this() 
    { 
     this.eventAggregator = eventAggregator; 
     this.eventAggregator.Subscribe(this); 
    } 

    // -------------------------------------------------------------------------------------------------------------------- 
    // Properties 
    // ------------------------------------------------------------------------------------------------------------------- 

    /// <summary>Gets or sets the character.</summary> 
    public Character Character { get; set; } 

    /// <summary>Gets or sets the character general view model.</summary> 
    public CharacterGeneralViewModel CharacterGeneralViewModel { get; set; } 

    /// <summary>Gets or sets the character metadata view model.</summary> 
    public CharacterMetadataViewModel CharacterMetadataViewModel { get; set; } 

    /// <summary>Gets or sets the character characteristics view model.</summary> 
    public CharacterApperanceViewModel CharacterCharacteristicsViewModel { get; set; } 

    /// <summary>Gets or sets the character family view model.</summary> 
    public CharacterFamilyViewModel CharacterFamilyViewModel { get; set; } 

    // -------------------------------------------------------------------------------------------------------------------- 
    // Methods 
    // ------------------------------------------------------------------------------------------------------------------- 

    /// <summary>Saves a character to the file system as a json file.</summary> 
    public void SaveCharacter() 
    { 
     ICharacterSaveService saveService = new JsonCharacterSaveService(Constants.CharacterSavePathMyDocuments); 

     saveService.SaveCharacter(this.Character); 

     this.characterTagsService.AddTags(this.Character.Metadata.Tags); 
     this.characterTagsService.SaveTags(); 
    } 

    /// <summary>Called when initializing.</summary> 
    protected override void OnInitialize() 
    { 
     this.CharacterGeneralViewModel = new CharacterGeneralViewModel(this.eventAggregator); 
     this.CharacterMetadataViewModel = new CharacterMetadataViewModel(this.eventAggregator, this.Character); 
     this.CharacterCharacteristicsViewModel = new CharacterApperanceViewModel(this.eventAggregator, this.Character); 
     this.CharacterFamilyViewModel = new CharacterFamilyViewModel(this.eventAggregator); 

     this.eventAggregator.PublishOnUIThread(new CharacterMessage 
     { 
      Data = this.Character 
     }); 


     base.OnInitialize(); 
    } 

    /// <summary> 
    /// Handles the message. 
    /// </summary> 
    /// <param name="message">The message.</param> 
    public void Handle(DataMessage<ICharacterTagsService> message) 
    { 
     this.characterTagsService = message.Data; 
    } 
} 

per il completamento Sake ho anche darvi uno dei ViewModel sub. Gli altri non hanno importanza perché sono strutturati nello stesso modo, svolgono solo compiti diversi.

/// <summary>The character metadata view model.</summary> 
public class CharacterMetadataViewModel : Screen 
{ 
    /// <summary>The event aggregator.</summary> 
    private readonly IEventAggregator eventAggregator; 

    /// <summary>Initializes a new instance of the <see cref="CharacterMetadataViewModel"/> class.</summary> 
    public CharacterMetadataViewModel() 
    { 
     if (Execute.InDesignMode) 
     { 
      this.Character = DesignData.LoadSampleCharacter(); 
     } 
    } 

    /// <summary>Initializes a new instance of the <see cref="CharacterMetadataViewModel"/> class.</summary> 
    /// <param name="eventAggregator">The event aggregator.</param> 
    /// <param name="character">The character.</param> 
    public CharacterMetadataViewModel(IEventAggregator eventAggregator, Character character) 
    { 
     this.Character = character; 

     this.eventAggregator = eventAggregator; 
     this.eventAggregator.Subscribe(this); 
    } 

    /// <summary>Gets or sets the character.</summary> 
    public Character Character { get; set; } 

    /// <summary> 
    /// Gets or sets the characters tags. 
    /// </summary> 
    public string Tags 
    { 
     get 
     { 
      return string.Join("; ", this.Character.Metadata.Tags); 
     } 

     set 
     { 
      char[] delimiters = { ',', ';', ' ' }; 

      List<string> tags = value.Split(delimiters, StringSplitOptions.RemoveEmptyEntries).ToList(); 

      this.Character.Metadata.Tags = tags; 
      this.NotifyOfPropertyChange(() => this.Tags); 
     } 
    } 
} 

ho già letto in su Screens, Conductors and Composition, IResult and Coroutines e scremato il resto della documentazione, ma in qualche modo non riesco a trovare quello che sto cercando.

// modifica: dovrei menzionare il codice che funziona bene. Non sono soddisfatto, poiché penso di non capire il concetto di MVVM abbastanza bene e quindi di fare codice difettoso.

+1

È normale avere un modello di visualizzazione che crea un'istanza di uno o più modelli di viste. – Sheridan

risposta

7

Non c'è niente di sbagliato nell'avere un ViewModel che istanzia diversi ViewModels secondari. Se stai costruendo un'applicazione più grande o più complessa, è praticamente inevitabile se vuoi mantenere il tuo codice leggibile e mantenibile.

Nel tuo esempio, stai istanziando tutti e quattro i ViewModels secondari ogni volta che crei un'istanza di CharacterViewModel. Ciascuno dei bambini ViewModels richiede IEventAggregator come dipendenza. Vorrei suggerire che si trattano questi quattro ViewModels bambino come dipendenze del primario CharacterViewModel e importarli tramite il costruttore:

[ImportingConstructor] 
public CharacterViewModel(IEventAggregator eventAggregator, 
          CharacterGeneralViewModel generalViewModel, 
          CharacterMetadataViewModel metadataViewModel, 
          CharacterAppearanceViewModel appearanceViewModel, 
          CharacterFamilyViewModel familyViewModel) 
{ 
    this.eventAggregator = eventAggregator; 
    this.CharacterGeneralViewModel generalViewModel; 
    this.CharacterMetadataViewModel = metadataViewModel; 
    this.CharacterCharacteristicsViewModel = apperanceViewModel; 
    this.CharacterFamilyViewModel = familyViewModel; 

    this.eventAggregator.Subscribe(this); 
} 

È possibile quindi rendere i setter sul bambino proprietà ViewModel privato.

Cambia i tuoi ViewModels bambino di importare IEventAggregator attraverso l'iniezione del costruttore:

[ImportingConstructor] 
public CharacterGeneralViewModel(IEventAggregator eventAggregator) 
{ 
    this.eventAggregator = eventAggregator; 
} 

Nel tuo esempio, due di questi bambini ViewModels sono passati un'istanza dei dati Character nelle loro costruttori, che implica una dipendenza. In questi casi, vorrei dare ad ogni bambino ViewModel un Initialize() metodo pubblico in cui impostare i dati Character e attivare la sottoscrizione di eventi aggregatore lì:

public Initialize(Character character) 
{ 
    this.Character = character; 
    this.eventAggregator.Subscribe(this); 
} 

quindi chiamare questo metodo nel metodo CharacterViewModelOnInitialize():

protected override void OnInitialize() 
{  
    this.CharacterMetadataViewModel.Initialize(this.Character); 
    this.CharacterCharacteristicsViewModel.Initialize(this.Character);  

    this.eventAggregator.PublishOnUIThread(new CharacterMessage 
    { 
     Data = this.Character 
    }); 


    base.OnInitialize(); 
} 

Per il bambino ViewModels in cui si aggiornano solo i dati Character tramite EventAggregator, lasciare la chiamata this.eventAggregator.Subscribe(this) nel costruttore.

Se uno qualsiasi dei vostri ViewModels bambino non sono in realtà necessari per la pagina di funzionare, si potrebbe inizializzare le proprietà delle macchine virtuali tramite proprietà di importazione:

[Import] 
public CharacterGeneralViewModel CharacterGeneralViewModel { get; set; } 

importazioni di proprietà non si verificano fino a dopo il costruttore ha completato in esecuzione .

Vorrei anche suggerire di gestire l'istanziazione di ICharacterSaveService tramite l'iniezione del costruttore, piuttosto che creare esplicitamente una nuova istanza ogni volta che si salvano i dati.

Lo scopo principale di MVVM era di consentire ai progettisti front-end di lavorare sul layout dell'interfaccia utente in uno strumento visivo (Expression Blend) e di codificatori per implementare il comportamento e il business senza interferire l'uno con l'altro. ViewModel espone i dati per essere associati alla vista, descrive il comportamento della vista a livello astratto e spesso funge da mediatore per i servizi di back-end.

Non esiste un modo "corretto" per farlo e ci sono situazioni in cui non è la soluzione migliore. Ci sono momenti in cui la soluzione migliore è lanciare il livello aggiuntivo di astrazione dell'utilizzo di un ViewModel e scrivere solo del code-behind. Quindi, benché sia ​​una grande struttura per l'applicazione nel suo complesso, non cadere nella trappola di forzare tutto per adattarsi al pattern MVVM. Se disponi di alcuni controlli utente più complessi graficamente in cui funziona semplicemente meglio avere code-behind, allora è quello che dovresti fare.

+1

Appena notato ora che questa domanda ha 5 mesi. Ops. Oh bene, spero che la mia risposta sarà ancora utile a qualcuno. – TeagansDad

+1

Sì, in realtà ha. Alcuni punti che ho già affrontato (il servizio di salvataggio ora utilizza il modello di repository), ma nel complesso devo dire grazie! – Ruhrpottpatriot