View Full Version : [C# - WPF]File Explorer: metodo più corretto per il pattern Model-View-ViewModel?
Donbabbeo
25-09-2010, 09:31
Salve a tutti, per necessità mi sono messo a studiare questo pattern e sto provando a realizzare la conversione di un'applicazione che avevo in WinForm.
L'applicazione è un semplice File Explorer, a sinistra l'elenco delle cartelle, a destra la lista dei files in modalità dettagli.
Ho cominciato per gradi, ho quindi da una parte un'applicazione wpf completa che mi carica i nodi dell'albero on demand impostando un path qualsiasi e dall'altra un programmino che mi carica tutti i file di una cartella preimpostata.
Ora devo fondere il tutto in un singolo programma e ho qualche dubbio su come seguire correttamente il pattern MVVM.
Ho creato 3 ViewModel: un ViewModel per ogni elemento del treeview (TreeItemViewModel), uno per ogni elemento della ListView (ListItemViewModel) e un ViewModel (MainViewModel) che è quello che associo come Datacontext alla Vista.
Quest'ultimo nel suo costruttore crea il nodo di root dell'albero e lo aggiunge alla lista dei TreeItemViewModel bindata con la vista (e fin qui nulla da dire) e inizializza la lista dei ListItemViewModel, anch'essa bindata con la vista, sempre al nodo di root.
Ora il problema è che quando clicco su un nuovo nodo, il rispettivo TreeItemViewModel viene richiamato ed eseguito, ma ovviamente il cambiamento non viene invocato poichè il MainViewModel non sa qual'è il nodo selezionato e allo stesso modo il TreeItemViewModel non ha conoscenza del MainViewModel...
Qual'è il metodo corretto? Devo aggiungere un membro alla MainViewModel che indica qual'è la cartella selezionata, aggiungendo al costruttore del TreeViewItemModel un riferimento al MainViewModel ad esso associato?
O esiste un metodo più elegante?
spero si capisca quel che voglio :mbe:
e allo stesso modo il TreeItemViewModel non ha conoscenza del MainViewModel...
Puoi farglielo conoscere. Non rompi il pattern.
Puoi fareglielo conoscere nel costruttore, come dici tu.
Oppure esponi un evento da parte dei TreeItemViewModel, intercettato dal MainViewModel.
Se fai cosi' ricordati di de-registrare tutte le sottoscrizioni.
Ora il problema è che quando clicco su un nuovo nodo, il rispettivo TreeItemViewModel viene richiamato ed eseguito,
Oppure... cosa usi per eseguire quel metodo nel TreeItemViewModel?
PS: Il file explorer c'e' gia' come controllo gia fatto :)
Donbabbeo
26-09-2010, 11:45
Oppure... cosa usi per eseguire quel metodo nel TreeItemViewModel?
Ho un presentation member booleano chiamato IsSelezionato, associato in two-way binding con il parametro IsSelected del TreeViewItem datatemplate.
Proverò così quindi: nel set di IsSelezionato aggiungerò un'istruzione per associare il nodo al MainViewModel. :D
PS: Il file explorer c'e' gia' come controllo gia fatto :)
Davvero? Mica lo sapevo... :stordita:
Donbabbeo
01-10-2010, 09:36
E' insorto un nuovo problema...
In pratica alla pressione di un tasto viene eseguita un'azione lunga e qundi volevo avviare una progress bar fino al termine dell'operazione.
Precisamente è una progress bar circolare sempre attiva, quindi agisco semplicemente sulla proprietà visibility del suo viewbox, così:
Visibility="{Binding ProgressBarShow, Converter={StaticResource BoolToVisibility}, UpdateSourceTrigger=PropertyChanged}"
La proprietà ProgressBarShow è quindi presente sul ViewModel nel modo più banale possibile:
public bool ProgressBarShow
{
get { return _progressbarshow; }
set
{
if (value != _progressbarshow)
{
_progressbarshow = value;
OnPropertyChanged("ProgressBarShow");
}
}
}
E quindi, alla pressione del tasto è associato un DelegateCommand di questo tipo:
public DelegateCommand RicercaCommand { get; private set; }
this.RicercaCommand = new DelegateCommand((o) => this.Ricerca(),null);
Ed il metodo ricerca è così definito:
public void Ricerca()
{
ProgressBarShow = true;
//Do stuff
Thread.Sleep(10000);
}
Io mi sarei aspettato che la progressbar venisse mostrata appena premuto il pulsante, invece viene mostrata correttamente al termine del metodo Ricerca.
Se invece inserisco un Debug.Print prima di ProgressBarShow=true, esso viene mostrato immediatamente. Avete qualche suggerimento? :muro:
Il metodo Ricerca() deve essere lanciato da un altro Thread.
Anche perche' senno' bloccheresti la GUI per tutta la durata della ricerca.
Donbabbeo
01-10-2010, 14:21
Il metodo Ricerca() deve essere lanciato da un altro Thread.
Anche perche' senno' bloccheresti la GUI per tutta la durata della ricerca.
Praticamente quello che facevo con le WindowsForm :doh:
Chissà che mi aspettavo con le WPF :fagiano:
Donbabbeo
05-10-2010, 10:59
Ok, ora non so veramente che pesci pigliare e non capisco cosa succede...
In pratica ho aggiunto un File System Watcher per monitorare la cartella corrente: quando un file viene eliminato, controllo la lista delle cartelle (è una proprietà del modelview stesso) e faccio ListaCartelle.Remove(file).
Ho fatto un pò di prove: il file esiste nella observable collection, viene trovato correttamente, ma quando cerco di rimuoverlo dalla lista, il programma crasha, stessa cosa se provo ad aggiungerne uno nuovo.
Il bello è che questo succede esclusivamente negli eventi del file system watcher, se provo a fare la stessa cosa tramite un comando associato ad un bottone funziona tranquillamente.
Da che può dipendere? :mbe:
L'evento del filesystemwatcher è questo:
void watcher_Deleted(object sender, FileSystemEventArgs e)
{
foreach (FileViewModel f in ListaFiles)
{
if (f.Nome == e.Name && f.Percorso == e.FullPath)
{
ListaFiles.Remove(f);
break;
}
}
}
Mentre ListaFiles è una semplice proprietà che espone una variabile privata della classe ed è una ObservableCollection di classi FileViewModel.
metti un try-catch e dicci l'eccezione, che con qualche probabilita' sara' la solita dove stai cercando di cambiare una property di un DependencyObject dal di fuori del Thread che ha creato il dependencyObject stesso.
Donbabbeo
05-10-2010, 18:14
metti un try-catch e dicci l'eccezione, che con qualche probabilita' sara' la solita dove stai cercando di cambiare una property di un DependencyObject dal di fuori del Thread che ha creato il dependencyObject stesso.
C'ho pensato appena salito in macchina... Domattina try-catcho e vi faccio sapere, oggi pomeriggio non ho avuto tempo.
Donbabbeo
06-10-2010, 09:31
Il messaggio è quello che ci si aspettava:
"This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread".
Ho letto un pò di documentazione sull'argomento, ma molte soluzioni mi sembrano fin troppo per il mio progetto. Ho provato quindi a farmi un delegato di questo tipo:
private delegate void RemoveFileDelegate(FileViewModel f);
private void RemoveFile(FileViewModel f)
{
ListaFiles.Remove(f);
}
void watcher_Deleted(object sender, FileSystemEventArgs e)
{
foreach (FileViewModel f in ListaFiles)
{
if (f.Nome == e.Name && f.Percorso == e.FullPath)
{
App.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new RemoveFileDelegate(RemoveFile), f);
}
}
}
Ed ora funziona, ma sarei curioso di sapere a quali problemi potrei andare in contro con questo metodo... :confused:
EDIT: così facendo, se elimino più di un file per volta crasha...
Il problema e' che la ObservableCollection e' un oggetto WPF, e non si dovrebbe usare nei ViewModel, altrimenti corri il rischio di sbattere di nuovo contro il problema della sincronizzazione, il cui dimenticarsi e' invece uno degli scopi di MVVM
Ma non puoi fare a meno di implementare cio' che WPF usa della ObervableCollection, altrimenti la collezione non viene rinfrescata sulla GUI.
In pratica la tua collezione deve essere una normale List (O qualunque ICollection del tipo che ti interessa) ma anche implementare INotifyCollectionChanged
Esistono online diverse implementazioni di una tale ObservableCollection lookalike che se userai, e se hai implementato MVVM correttamente, ti risolveranno il problema nel modo migliore.
Per iniziare e per vedere se stai andando nella direzione giusta, prima di cercare e usare una di queste nuove ObservableCollection, usa una List<> normale.
Semplicemente lancia NotifyPropertyChanged ogni volta che modifichi il contenuto della lista.
L'intera lista verra' quindi rinfrescata sulla GUI, il che non e' ottimale, ma almeno non dovresti piu' ricevere l'errore.
Potrai quindi proseguire sostituendo la List<> con il nuovo tipo corretto per beneficiare al meglio della messaggistica.
Donbabbeo
06-10-2010, 14:13
Il problema e' che la ObservableCollection e' un oggetto WPF, e non si dovrebbe usare nei ViewModel, altrimenti corri il rischio di sbattere di nuovo contro il problema della sincronizzazione, il cui dimenticarsi e' invece uno degli scopi di MVVM
Ma non puoi fare a meno di implementare cio' che WPF usa della ObervableCollection, altrimenti la collezione non viene rinfrescata sulla GUI.
In pratica la tua collezione deve essere una normale List (O qualunque ICollection del tipo che ti interessa) ma anche implementare INotifyCollectionChanged
Esistono online diverse implementazioni di una tale ObservableCollection lookalike che se userai, e se hai implementato MVVM correttamente, ti risolveranno il problema nel modo migliore.
Per iniziare e per vedere se stai andando nella direzione giusta, prima di cercare e usare una di queste nuove ObservableCollection, usa una List<> normale.
Semplicemente lancia NotifyPropertyChanged ogni volta che modifichi il contenuto della lista.
L'intera lista verra' quindi rinfrescata sulla GUI, il che non e' ottimale, ma almeno non dovresti piu' ricevere l'errore.
Potrai quindi proseguire sostituendo la List<> con il nuovo tipo corretto per beneficiare al meglio della messaggistica.
Grazie mille, vedrò come correggerla seguendo il tuo consiglio :D
PS: usando una comune List di FileViewModel e invocando OnPropertyChanged ad ogni suo cambiamento, il programma funziona normalmente :D
Donbabbeo
06-10-2010, 14:51
RETTIFICA: L'inserimento e la rimozione, sebbene funzionino (non essendo una ObservableCollection posso invocare semplicemente Remove ed Add senza problemi), non vengono propagati alla View.
Provo a dire quello che mi sembra, correggimi se dico una cazzata: la OnpropertyChanged della List, controlla la lista come proprietà nella sua interezza, quindi scatena il suo metodo se e solo se la lista cambia nella sua interezza, cioè l'intero contenuto viene modificato, difatti il popolamento e la ricerca funzionano perchè io azzero e poi ricarico l'intera lista. Al contrario, la rimozione e l'inserimento non modificano la lista intera, ma un suo elemento interno, perciò senza la OnCollectionChanged non ho possibilità di aggiornare la lista. Sbaglio?
Ora implemento anche INotifyCollectionChanged nella mia ViewModelBase, se ho ben capito, inserendo OnCollectionChanged dopo l'inserimento o la rimozione dalla lista, dovrei vedere l'azione propagata alla UI. E' corretto il mio ragionamento?
RETTIFICA: L'inserimento e la rimozione, sebbene funzionino (non essendo una ObservableCollection posso invocare semplicemente Remove ed Add senza problemi), non vengono propagati alla View.
Provo a dire quello che mi sembra, correggimi se dico una cazzata: la OnpropertyChanged della List, controlla la lista come proprietà nella sua interezza, quindi scatena il suo metodo se e solo se la lista cambia nella sua interezza, cioè l'intero contenuto viene modificato, difatti il popolamento e la ricerca funzionano perchè io azzero e poi ricarico l'intera lista. Al contrario, la rimozione e l'inserimento non modificano la lista intera, ma un suo elemento interno, perciò senza la OnCollectionChanged non ho possibilità di aggiornare la lista. Sbaglio?
Rimozione o inserimento non cambiano la lista.
Sei tu che devi a mano lanciare OnPropertyChanged("NomedellaLista") ogni volta che aggiungi o rimuovi qualcosa dalla lista.
Con tutti i contro del caso sulla gui, ovvero rinfrescare completamente da zero tutto l'elenco.
Ora implemento anche INotifyCollectionChanged nella mia ViewModelBase, se ho ben capito, inserendo OnCollectionChanged dopo l'inserimento o la rimozione dalla lista, dovrei vedere l'azione propagata alla UI. E' corretto il mio ragionamento?
No, devi costruire un nuovo oggetto, chiamato MiaLista, che implementi sia IList<> che INotifyCollectionChanged<>
E poi dovrai usarlo nel tuo viewmodel originale al posto della ObservableCollection.
Poiche' sia IList<> che INotifyCollectionChanged<> sono abbastanza lunghi, ti consiglio di usare qualcosa di gia' fatto.
Ma ti consiglio questo passo solo dopo essere riuscito ad usare la semplice List<> di cui sopra.
Donbabbeo
06-10-2010, 15:28
Prima di tutto: grazie per l'aiuto e scusa se ti rompo, ma voglio capire bene come funziona la cosa :O
Rimozione o inserimento non cambiano la lista.
Sei tu che devi a mano lanciare OnPropertyChanged("NomedellaLista") ogni volta che aggiungi o rimuovi qualcosa dalla lista.
Con tutti i contro del caso sulla gui, ovvero rinfrescare completamente da zero tutto l'elenco.
Allora, io ho provato a sostituire la ObservableCollection con una List di files.
Quando io la popolo faccio:
NomeLista.Clear();
NomeLista = caricaLista();
OnPropertyChanged("NomeLista");
dove caricaLista è un metodo che mi ritorna una List. Questo funziona correttamente e la lista sulla UI è di fatto aggiornata.
Quando inserisco un nuovo elemento faccio
NomeLista.Add(new Elemento(parametri elemento));
OnPropertyChanged("NomeLista");
ma in questo caso l'aggiornamento, sebbene il nuovo elemento sia presente, non viene visualizzato nell'interfaccia. Stessa cosa ovviamente per la rimozione.
E' normale che faccia così?
vBulletin® v3.6.4, Copyright ©2000-2025, Jelsoft Enterprises Ltd.