Scusa se ho ritardato un po', ma stavo finendo l'articolo che ho allegato alla risposta...
In teoria ci sono tanti modi per fare quello che dici, dal metodo più a basso livello fino a quello più sosfisticato ed elegante sfruttando le potenzialità di WPF e del suo modello fatto di DependencyProperty con le derivate AttachedProperty, Commanding e Binding, e quindi in pratica, ti direi fin da subito di sfruttare queste potenzialità.
Stringendo a basso livello potresti semplicemente crearti una MainWindow e un UserControl, esattamente come si faceva in windows form, e poi sulla MainWindow inserire un elemento placeholder, anche qui come faresti in windows form se ti serve sapere in che punto preciso iniettare l'usercontrol, oppure semplicemente, nel code behind della MainWindow, crearti l'instanza dell'UserControl e iniettarlo direttamente sulla MainWindow così:
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
var startScreenView = new StartScreenView();
this.AddChild(startScreenView);
}
}
dove MainView è una semplice Window (WPF) e StartScreenView è un User Control (WPF).
ma.... non ti consiglio per niente questo approccio, non ha senso con le potenzialità offerte da WPF, e gli strumenti che direttamente e indirettamente ci offre per fare un eccezionale UI composition (http://blogs.ugidotnet.org/topics/archive/2009/03/27/ui-composition-thread.start.aspx) o per lo meno una gestione dinamica degli user control, senza avere troppo codice da manutenere, e sopratutto cercando di slegare ogni componente tra di loro, rendendoli autonomi, e facendoli comunicare tra di loro sfruttando un message broker, che semplicisticamente si può definire come un oggetto statico, che può essere utilizzato da qualsiasi parte nell'applicazione, e che permette di lanciare un messaggio all'interno della stessa, poi se c'è qualcuno che è in ascolto di quel messaggio gli verrà recapitato ed eseguirà quello che deve, in questo modo in pratica capisci come una main Window può comunicare con i suoi figli UserControl, senza necessariamente doverli conoscere, si scambiano messaggi attraverso un terzo attore, quindi nel tuo caso una MainView aprirà di default una sorta di StartScreenView, questa view avrà le tile, che quando sono premute, spareranno un messaggio al broker, dichiarando l'intenzione dell'utente nell'aprire un'altra view (app), la MainView già prima si era sottoscritta a quel tipo di messaggio sfruttando il message broker (NON alla view, quindi nessuna dipendenza!), quindi dicevamo la MainView riceve il messaggio, istanzia la vista richiesta dal messaggio (ovviamente la MainView conosce cosa fare con il messaggio!) e la inietta nel placeholder o direttamente come unico figlio della window. Stessa cosa per chiudere una vista e tornare allo StartScreen, ci sarà un messaggio GoToStartScreenMessage, che ogni vista che vuole dare la possibilità all'utente di tornare allo startscreen conosce, e che semplicemente lancierà al broker, alla ricezione del messaggio da parte della MainWindow, la stessa mostrerà di nuovo lo StartScreenView e successivamente metterà in background la precedente vista, esattamente come fa Windows 8, da qui si capisce quanto può essere potente WPF, che ti permette con davvero poche righe di codice, ma ben studiate, di ricreare una parte del sistema operativo di windows 8, dove puoi riuscire tranquillamente a riscriverti i principi di background, foreground e chiusura delle tue sotto viste, esattamente come se fosse applicazioni windows store (metro app), anche se sono dei semplici moduli della tua applicazione, ma vista nell'ottica di tante sotto applicazioni, capisci quanti vantaggi può darti, ti permette di lavorare chiuso in una funzionalità dell'applicazione senza dovere pensare a tutte le iterazioni con le altre, e un altro enorme vantaggio anche nella collaborazione di un team e nella manutenibilità del codice nel tempo, per non parlare della possibilità di fare un software multi-licence, dove regalare la shell (MainWindow), ma fare pagare ogni singolo modulo/app.
ok sono stato super logorroico, ma spero di averti aperto un piccolo mondo.
Ritornando all'approccio che ti consiglierei, lascia perdere il code behind delle window/usercontrol, cerca inizialmente di pensare come se non lo avessi a disposizione (*1), e cerca di studiararti il MVVM (INotifyPropertyChanged), Commanding (ICommand), Dependency property (DependencyProperty, AttacchedProperty) e bene come funziona lo XAML che ti aprirà un vero e proprio mondo, cerca in particolare le keyword: DataContext, Binding, ControlTemplate e DataTemplate (tantissimi concetti li trovi scritti in italiano e bene sul forum di Mauro Servienti http://milestone.topics.it che dopo tutta questa pubblicità almeno mi deve una birra!! e io a lui perchè mi fa comodo :) ). Nel frattempo per avere un applicazione pronta all'utilizzo potresti già pensare di utilizzare un framework MVVM completo come Radical (ce ne sono tanti altri, ma per progetti nuovi io consiglierei questo, non perchè sia più potente o cazzate del genere, semplicemente perchè è stato riscritto da zero prendendo spunto da tutti gli altri framework, cercando di crearne uno più "pulito", per il semplice fatto che è nuovo e non deve sottostare alla richiesta di retrocompatibilità), questi sono i punti per arrivare a quello che chiedevi, poi da li, tanto studio e in bocca al lupo:
- seguire le istruzioni del mio post su radical: http://blogs.dotnethell.it/Regulator/Tutorial-WPF-MVVM-in-un-Minuto-con-Radical__19072.aspx
- aggiungere altri 2 items, all'interno della cartella Presentation, per la shell: StartScreenView.xaml (UserControl) e StartScreenViewModel.cs (Class)
- aggiungere altri 2 items, all'interno della cartella Presentation, di prova per aggiungere un modulo/app: HelloWorldView.xaml (UserControl) e HelloWorldViewModel.cs (Class)
- nella MainView.xaml mettere il ContentPresenter per abilitare la Dynamic UI Composition, ovvero il placeholder della shell su cui iniettare i vari moduli:
- nello StartScreenView.xaml mettere una cosa così (puramente a scopo illustrativo):
- nello StartScreenViewModel.cs si gestisce semplicemente il comando di OpenView, chiedendo al broker di lanciare un messaggio di tipo OpenHelloWorldViewMessa():
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfApplication1.Messaging;
namespace WpfApplication1.Presentation
{
public class StartScreenViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenView { get; private set; }
public StartScreenViewModel(IMessageBroker messageBroker)
{
this.messageBroker = messageBroker;
this.OpenView = DelegateCommand.Create()
.OnExecute(p => OpenViewHandler());
}
public void OpenViewHandler()
{
Debug.WriteLine("OpenViewHandler()");
this.messageBroker.Broadcast(this, new OpenHelloWorldViewMessage());
}
}
}
- Nella vista HelloWorldView.xaml metti quello che vuoi più il pulsante per tornare allo start screen:
- Il view model HelloWorldViewModel.cs è praticamente identico all'altro, solo che cambia il tipo di messaggio lanciato in OpenStartScreenViewMessage():
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfApplication1.Messaging;
namespace WpfApplication1.Presentation
{
public class HelloWorldViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenStartScreen { get; private set; }
public HelloWorldViewModel(IMessageBroker messageBroker)
{
this.messageBroker = messageBroker;
this.OpenStartScreen = DelegateCommand.Create()
.OnExecute(p => OpenStartScreenHandler());
}
public void OpenStartScreenHandler()
{
Debug.WriteLine("OpenStartScreenHandler()");
this.messageBroker.Broadcast(this, new OpenStartScreenViewMessage());
}
}
}
- Aggiungi una cartella Messaging
- Aggiungi le classi per i due messaggi:
OpenHelloWorldViewMessage.cs
OpenStartScreenViewMessage.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfApplication1.Presentation;
namespace WpfApplication1.Messaging
{
public class OpenHelloWorldViewMessage
{
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1.Messaging
{
public class OpenStartScreenViewMessage
{
}
}
in questo caso i messaggi non trasportano nessun'altra informazione con se, ma quasi sempre lo fanno.
- Creare una cartella Handlers all'interno della cartella Messaging (è importantissimo che la struttura sia esattamente come ti sto dicendo, perchè sono delle regole di default che utilizza Radical, che possono anche essere cambiate, ma ovviamente è tutto più veloce se le si segue, come funziona anche per ASP.NET MVC se hai dimestichezza con le regole di routing dei Controller)
- Crea sotto la cartella Handlers la gestione dei messaggi:
ApplicationBootCompletedHandler.cs
Questo è un messaggio di "sistema" che genera Radical non appena ha finito di eseguire il suo startup.
OpenHelloWorldViewMessageHandler.cs
OpenStartScreenViewMessageHandler.cs
- Il tutto dovrebbe magicamente funzionare. Ovviamente non c'è niente di magico, ci sono tante righe di codice e strumenti utilizzati in Radical che facilitano il lavoro dello sviluppatore WPF, una cosa che salta subito all'occhio che non esistono punti dove si vedono istanziazioni degli oggeti viewResolver, regionService, e in generale tutte le dipendenze dei vari ViewModel, questo perchè Radical utilizza un framework (Castle) di IoC (inversion of control) per eseguire la DI (dependency injection), e con una serie di regole ben definite, per cui è sembre in grado di mantenere una relazione tra view e viewmodel, e inoltre in ogni punto dell'applicazione dove si necessita di un qualsiasi componente infrastrutturale di Radical, è sufficiente definirlo come dipendenza nella classe stessa, ovviamente se la stessa è attivata dal flusso di Radical-Castle (quindi utilizzando ad esempio il viewResolver).
Ti ho svalangato un sacco di nozioni, dato un esempio pratico, ora spetta a te approfondire, perchè ovviamente c'è troppo da dire per una risposta a un thread.
Probabilmente da questa risposta farò un esempio completo che pubblicherò sul mio blog, riguardante appunto la UI Composition con Radical, ma non ti so dire la data di pubblicazione, collegati all'RSS per rimanere in ascolto.
Ciao!!
*1 - Non voglio dire che il code behind non va mai usato, ma in pratica si utilizza principalmente se si sta creando un vero e proprio CustomControl, che comunque sconsiglio di pensare subito alla creazione di un CustomControl non appena pensiamo non ci sia il controllo che ci serve in quelli di WPF, perchè il 90% dei casi e forse di più, il controllo già esiste nella libreria di WPF, solo che non lo si riesce a vedere, nel senso che, bisogna pensare alla libreria di controlli wpf come solo un punto di partenza, non l'arrivo, l'arrivo che ci danno sono solo 2 schifosi controlli leggermente stilizzati in base al sistema operativo in cui siamo, però la realtà è che il grosso lavoro sta nello sviluppo dei comportamenti dei controlli, cioè un button, può diventare una tile in pochissimo tempo di "sviluppo", in realtà non è neanche sviluppo è styling in xaml, si prende il button, gli si crea un altro template grafico, ma si mantengono tutte le potenzialità dei comportamenti del button che non dobbiamo riscrivere, ad esempio il fatto che ci venga eseguito un Command nel momento in cui premiamo il pulsante! Quindi prima di fare un CustomControl, pensa se davvero hai bisogno di scrivere dei comportamenti che già esistono nei controlli standard, perchè un custom control ha del valore aggiunto se ha dei comportamenti (behaviour) nuovi, non nell'interfaccia grafica!
-------------------------------------------------------
Michael Denny
Lead Software Developer & Solutions Architect
http://blogs.dotnethell.it/Regulator/