Post recenti
Beth Massi ha pubblicato tre nuovi video formativi della serie "How-do-I", su Visual Studio LightSwitch. Si tratta di video che riguardano relazioni master-details nelle maschere, passaggio di parametri alle maschere, regole di business per la validazione e campi calcolati. Trovate l'elenco completo in questo suo post.
Alessandro
Tramite Windows Update viene in questi giorni distribuito un aggiornamento per Silverlight 4 che risolve alcune incompatibilità tra la versione corrente e quella installata con Visual Studio LightSwitch, che causavano problemi nel deploy di applicazioni LightSwitch su altre macchine diverse da quella di sviluppo.
Dovreste ricevere l'aggiornamento in automatico, se avete già Silverlight sulla macchina (scenario molto probabile :-)) diversamente, al primo accesso a un'applicazione Silverlight, verrà automaticamente installata la versione che risolve i bug. Ulteriori dettagli sul blog del team di LightSwitch qui.
Alessandro
Il Team di Windows Phone 7 ha annunciato che il sistema operativo è giunto alla RTM, ossia il rilascio definitivo. Una notizia sicuramente importante per tutti i fruitori e gli sviluppatori per dispositivi mobili, che precede il rilascio previsto per il giorno 16 settembre dei tool di sviluppo per Visual Studio 2010, sempre in RTM.
Ora aspettiamo i dispositivi però! :-)
Alessandro
Probabilmente lo sapete già se ricevete le newsletter delle nostre community o se ci seguite su Facebook/Twitter, ma nel caso in cui non lo sappiate e foste interessati, giovedì 2 settembre alle ore 21 terrò una sessione gratuita online per conto della nuova community LightSwitch Tips & Tricks, nella quale faremo un'introduzione a Visual Studio LightSwitch beta 1 e all'interno della quale vedremo non solo quali sono le caratteristiche di questo tool ma faremo anche delle demo per vedere come costruire applicazioni Line Of Business con pochi passaggi.
L'idea è anche quella di mostrare come personalizzare le applicazioni con custom validation, campi calcolati e query soprattutto per invogliare gli sviluppatori VB 6 a dare un'occhiata a cosa si perdono non utilizzando .NET :-) Sarà anche un'occasione per rispondere alle vostre domande e alle vostre curiosità. Le slide sono poche, ci concentriamo di più sulla pratica. Le cose da far vedere sarebbero tantissime, inizieremo con questo incontro e se troverà il vostro gradimento ne pianificheremo degli altri anche sulla base dei feedback.
Le credenziali di accesso al Live Meeting sono disponibili all'interno del sito LightSwitch Tips & Tricks. Vi aspetto giovedì sera!
Alessandro
Uno dei primi thread sul nuovo sito LightSwitch Tips & Tricks riguardava un errore verificatosi in corso di installazione della Beta 1 di Visual Studio LightSwitch. In particolare, la procedura di setup si interrompeva rilevando la mancanza dei WCF RIA Services sulla macchina.
In realtà, il setup di LightSwitch dovrebbe automaticamente installare i citati componenti se non li rileva, ma è un qualcosa che in questi casi si chiama known issue. Niente di grave, comunque. Se avete scaricato l'ISO completa, l'immagine del DVD contiene una cartella WCU, dove sono locati tutti i prerequisiti. Nella sottocartella Silverlight trovate un file chiamato RIAServices.msi. E' sufficiente installarlo e ripetere l'installazione di LightSwitch.
Se invece avete utilizzato il Web installer, è necessario scaricare separatamente i tool di Silverlight 4 per Visual Studio 2010, quindi, dopo averli installati, ripetere l'installazione.
Alessandro
Ammetto che ci stavo rinunciando, fortunatamente grazie a Twitter oggi ho scoperto che è stata rilasciata una patch per WPF Performance Suite, il tool di analisi delle performance in applicazioni WPF fornito con Windows SDK, che corregge un problema secondo cui su sistemi ove sono impostati particolari time zone (come il nostro), non veniva visualizzato alcun grafico di analisi.
Installata la patch, il problema si risolve e tutto torna a funzionare a dovere. La fonte originale è questa.
Se non avete mai sentito parlare di WPF Performance Suite, state sintonizzati sul nostro sito WPF Tips & Tricks perché a breve ci sarà del materiale al riguardo.
Alessandro
Un grosso problema allo stato attuale è che i tool di sviluppo per Windows Phone 7 supportano solo Visual C#. Quindi Visual Basic e F# sono esclusi, almeno per il momento, e non si hanno notizie in merito anche tenuto conto del fatto che il 16 settembre i tool in questione saranno in RTM.
Justin Angel ha scritto un post in cui dimostra come utilizzare entrambi i linguaggi all'interno di un'applicazione per Windows Phone scritta in C#, lavorando su class library che poggiano su Silverlight 3.
Interessante lettura, a mio avviso, che trovate qui.
Alessandro
In concomitanza con il rilascio della Beta 1 di Visual Studio LightSwitch, il relativo team di prodotto ha anche pubblicato un Training Kit per imparare ad utilizzare LightSwitch in modo produttivo. Tale materiale include documenti e codice sorgente per cominciare a sviluppare applicazioni data-centric con Visual Studio LightSwitch, partendo da una semplice applicazione fino ad applicazioni complesse, incorporando anche controlli utente personalizzati.
Il training kit si trova a questo indirizzo.
Approfitto di questo post per ringraziare tutti coloro che si stanno iscrivendo a LightSwitch Tips & Tricks: in circa 40 ore di vita, il nostro sito annovera già 136 utenti registrati! Segno che Visual Studio LightSwitch è un prodotto di sicuro interesse per lo sviluppo di applicazioni gestionali, anche se per ora solo in beta.
Alessandro
Come annunciato da Lorenzo Barbieri nel proprio Blog del Team di MSDN Italia il 16 settembre (meno di un mese) verrà rilasciata la versione RTM (finale) dei Tools di Sviluppo per Windows Phone7
Nel dettaglio verranno rilasciati Visual Studio ed Expression Blend per Windows Phone 7 e saranno inclusi i controlli Pivot, Panorama ed il supporto nativo per Bing Maps.
Renato Marzaro
- Uno sviluppatore impiega circa il 10-20% del suo tempo a scrivere codice, e di tutto il codice scritto al giorno dalla maggior parte degli sviluppatori, solo circa 10-12 linee di codice resteranno nel prodotto finale, indipendentemente dalle capacità dello sviluppatore stesso. Un buon sviluppatore passa la maggior parte del 90% del tempo restante pensando, ricercando, e sperimentando allo scopo di ottenere un design migliore. Un cattivo sviluppatore passa la maggior parte del 90% del tempo restante debuggando il codice, facendo modifiche random, e verificando che tutto funzioni.
“A great lathe operator commands several times the wage of an average lathe operator, but a great writer of software code is worth 10,000 times the price of an average software writer.” –Bill Gates
- Un buon sviluppatore è 10 volte più produttivo di uno sviluppatore medio. Un ottimo sviluppatore è 20-100 volte più produttivo della media. Non sto esagerando, – studi effettuati sin dal 1960 hanno dimostrato più e più volte questa affermazione. Un cattivo sviluppatore non è soltanto poco produttivo: oltre a non portare a termine i propri compiti, spesso e volentieri crea problemi che sta poi agli altri correggere (a forza di mal di testa).
- Un ottimo sviluppatore dedica veramente poco tempo alla scrittura del codice – o almeno del codice che va a finire nel prodotto finale. Uno sviluppatore che impiega la maggior parte del proprio tempo a scrivere codice è uno sviluppatore pigro, troppo ignorate, o troppo arrogante per cercare soluzioni già pronte ai problemi che incontra. Un ottimo sviluppatore è in grado di riconoscere e riutilizzare i pattern comuni. Un buon sviluppatore non ha paura di refactor-izzare costantemente il proprio codice per raggiungere il design ideale. Un cattivo sviluppatore scrive codice privo di integrità concettuale, ridondante, non gerarchico e senza pattern, e per questo molto difficile da refactorizzare. Si fa prima a buttar via il vecchio codice e ricominciare d’accapo che a modificarlo.
- […]
- […]
- Anche se la maggior parte dei software è realizzato da team di sviluppo, la realizzazione di un sistema software non è un’attività democratica. Tipicamente, c’è un solo responsabile della progettazione del sistema: il resto del team lavora sui dettagli.
- La programmazione è un lavoro duro. E’ un’attività mentale intensa. Un buon sviluppatore pensa al proprio lavoro 24/7. Le idee migliori arrivano sotto la doccia, o dormendo. Dato che la parte più importante del lavoro avviene lontano dalla tastiera, la realizzazione di un software non può essere accelerata passando più tempo in ufficio o aggiungendo persone al progetto.
Fonte: http://dotmac.rationalmind.net/2010/08/some-lesser-known-truths-about-programming/
Dopo l'annuncio da parte di Microsoft Corp. della disponibilità per il pubblico del download della Beta 1 di Visual Studio LightSwitch, anche noi di Visual Basic Tips & Tricks facciamo il nostro annuncio :-) comunicando ufficialmente la nascita di "LightSwitch Tips & Tricks" (www.lightswitch.it), nuova community del network di VB T&T dedicata in modo specifico al nuovo prodotto della famiglia di Visual Studio 2010.

La nuova community ha lo scopo di aggregare e condividere le conoscenze su Visual Studio LightSwitch, anche nell'ottica del fatto che molti sviluppatori VB 6/VBA saranno interessati a dare un'occhiata da vicino al nuovo tool di casa Microsoft per dare una svolta definitiva verso la migrazione a .NET.
Il nostro Team (io, Renato, Antonio e Diego) ha lavorato sodo in questi ultimi giorni per sviluppare portale & contenuti. Abbiamo infatti dei forum in cui potremo discutere di LightSwitch ma abbiamo anche già una serie di materiale da segnalarvi per la lettura o la visione. Qualche articolo (in particolare traduzioni autorizzate dal Team di Visual Studio LightSwitch a Redmond):
E i primi due video che ho realizzato personalmente nei giorni scorsi:
-
Due chiacchiere su Visual Studio LightSwitch
-
Sviluppare applicazioni data-centric con Visual Studio LightSwitch (beta 1)
Nota importante, i video sono anche scaricabili per la visione off-line. L'accesso alle aree Contenuti e Video è riservato agli utenti registrati, per cui potete iscrivervi (gratuitamente) al sito della nuova community.
Speriamo che possiate trovare utile il nuovo sito. Personalmente credo che LightSwitch sia un bel tool che incontrerà il favore di molti sviluppatori. Chiaramente abbiamo già in cantiere molte iniziative dedicate a Visual Studio LightSwitch, quali Live Meeting e altre cosine interessanti.
Vi invito quindi a visitare il nuovo sito e.. ci vediamo anche lì! ;-)
Alessandro
Il team di Visual Studio LightSwitch ha appena rilasciato la prima Beta del nuovo ambiente di sviluppo per applicazioni data-centric, per il download pubblico. Il post originale del Team di LightSwitch, con l'annuncio, si trova qui.
Vi segnalo anche la disponibilità odierna di alcuni "How-do-I videos" realizzati dalla grandissima Beth Massi (che ha anche fatto un suo annuncio qui) nonché alcuni tutorial nuovi, per imparare ad utilizzare il prodotto. State sintonizzati, ci sentiamo tra pochi minuti con un'altra novità inerente LightSwitch :-)
L'elenco completo dei materiali si trova sul LightSwitch Developer Center.
Alessandro
Volevo informarvi del fatto che tutti i nostri video screencast su Windows Presentation Foundation e Windows Phone, disponibili nel sito della community WPF Tips & Tricks, sono finalmente disponibili per il download, oltre che per la visione online, di modo che possiate riguardarli anche offline in comodità. Vi ricordo che l'accesso all'area Contenuti, dove risiedono i download, è riservato agli utenti registrati.
In particolare, per accedere ai download dei video potete sfogliare la categoria Video dell'area Contenuti, accessibile direttamente da questo link.
Personalmente sono sempre stato molto convinto dell'utilità di far scaricare i video e abbiamo ricevuto molti feedback al riguardo, per cui finalmente lo abbiamo fatto! Enjoy! :-)
Alessandro
Molti di voi ormai sapranno che poco tempo fa Microsoft ha annunciato un nuovo prodotto della famiglia Visual Studio, chiamato Visual Studio LightSwitch che dal giorno 23 agosto sarà disponibile in beta 1 per il download pubblico e che è gia disponibile per gli abbonati MSDN. In questo post descriverò un po' LightSwitch, con poche parole e molte immagini.
Cos'è Visual Studio LightSwitch (e un pensiero per gli sviluppatori VB 6)
Visual Studio LightSwitch è un ambiente di sviluppo per lo sviluppo rapido di applicazioni Line Of Business, quelle che in gergo potremmo chiamare "gestionali". Rapido perché si usa la filosofia: "creo le tabelle -> creo le maschere -> LightSwitch prepara l'infrastruttura -> eseguo ed ottengo l'applicazione funzionante anche senza scrivere codice". Una filosofia forse familiare a chi proviene da sviluppo con Access/VBA ma sicuramente interessante per chi proviene dal mondo VB 6, una ghiotta occasione per migrare a .NET con tecnologie nuove e moderne e in modo veloce.
A chi è destinato
VisualStudio LightSwitch è destinato principalmente a:
Cosa sfrutta
Le applicazioni LightSwitch utilizzano, dietro le scene, tecnologie e pattern d'avanguardia anche se non necessariamente lo sviluppatore è tenuto a saperlo: Silverlight 4, WCF RIA Services, SQL Server Express, SharePoint 2010, SQL Azure, pattern MVVM, architettura N-Tier.
Come funziona
Splash screen d'avvio:
Pagina iniziale e ambiente con la stessa familiarità di Visual Studio 2010:
Le possibilità di creare applicazioni sono davvero semplici: VB o C#:
Dapprima si sceglie come creare le origini dati, partendo da zero o connettendosi a dati esistenti (tra questi annoveriamo SQL Server, SharePoint 2010, WCF RIA Services):
Ipotizzando di partire da zero, creo una nuova tabella. Si notino due nuovi tipi di dato specifici per email e numeri di telefono. Sono importanti perché incorporano funzionalità di validazione dei dati:
Una volta che ho i dati, scelgo di creare le maschere. Ho a disposizione una serie di template predefiniti. Seleziono dapprima una maschera per mostrare l'elenco dei miei elementi:
Quindi una per la creazione e aggiunta di un nuovo elemento:

Il designer mostrerà la composizione della maschera, elencando i controlli di comando e quelli data-bound:
Mi basta premere F5 e la mia applicazione già funziona:
Si tratta di un'applicazione Silverlight 4 che viene eseguita nel desktop come "Out-of-browser", che memorizza i dati in un database SQL Server che LightSwitch ha generato per l'applicazione. Si notino:
-
un controllo Ribbon con i comandi principali
-
una scheda Task con i comandi per mostrare le maschere
-
validazione automatica dei dati, senza che abbiamo scritto codice
Possiamo visualizzare poi l'elenco degli elementi:

La maschera include per default:
-
possibilità di ricerca/filtro
-
comando di esportazione in Microsoft Excel
-
paginazione dei dati in basso alla maschera
Se decido di creare una nuova tabella da associare alla precedente, stabilire una relazione è semplicissimo:

Le applicazioni LightSwitch sono 2-tier di default, ma posso modificarle come 3-tier desktop (passando per IIS) o 3-tier in-browser:
Utilizzando l'opzione in-browser, l'applicazione Silverlight viene eseguita nel mio browser Web:

Visual Studio LightSwitch non è assolutamente solo quello che vi ho mostrato. Si può scrivere codice anche complesso, query LINQ, impostare validazioni personalizzate, personalizzare l'interfaccia a runtime, accedere a SharePoint, creare enumerazioni nelle tabelle. Tutto questo lo approfondiremo più avanti, soprattutto a partire da martedì 23 agosto in cui non solo ci saranno nuovi materiali sul LightSwitch Developer Center, ma anche giorno in cui noi di VB T&T faremo un annuncio tutto italiano :-)
A presto!
Alessandro

E' finalmente disponibile per il download, per i soli abbonati MSDN, Microsoft Visual Studio LightSwitch beta 1. Il download pubblico sarà aperto il giorno 23 p.v.
Ma cos'è LightSwitch? Si tratta di un nuovo prodotto della famiglia Visual Studio 2010, dedicato allo sviluppo rapido di applicazioni Line Of Business (LOB), quelle che chiameremmo tradizionalmente applicazioni gestionali ed è dedicato a coloro che vogliono sviluppare rapidamente e senza troppi interventi applicazioni data-centric secondo lo stile: crea/importa i dati -> genera una maschera di inserimento/modifica/visualizzazione, come quella mostrata nella seguente figura:
Quindi piccole aziende, sviluppatori con poca esperienza o con poca necessità di avere skill avanzati, troveranno in LightSwitch un prodotto ideale.
Sicuramente è un prodotto che farà breccia nel cuore degli sviluppatori VB 6 che hanno interesse a migrare a .NET. Attraverso il familiare ambiente di sviluppo di Visual Studio 2010, di cui LightSwitch incorpora la shell, Si possono creare sorgenti dati da zero, basate su SQL Server, oppure importare dati da più origini come SQL Server, SQL Azure, SharePoint 2010 e generare maschere e applicazioni complete senza scrivere una sola riga di codice.
Vi segnalo alcuni link utili:
LightSwitch Developer Center
LightSwitch Beta 1 Documentation on MSDN
LightSwitch Forum
LightSwitch on Channel 9
VisionClinic application Walkthrough
Post in italiano di Pietro Brambati
In particolare, oltre a segnalarvi la data del 23 agosto per il download aperto al pubblico, vi invito a visitare i blog di VB T&T nello stesso giorno perché avremo un annuncio importante da fare, proprio con riferimento a Visual Studio LightSwitch.
Ci sentiamo presto!
Alessandro
Ecco la prima versione di CyberUpdater, con dei moduli a caso, così come i numeri di versione, tanto per il debug:
Notare che tutto è ampiamente personalizzabile: nome del software, autore, moduli, possibilità di indicare un modulo che rappresenta la suite, licenza gratuita o a pagamento, ecc.. Tutto è stato pensato per garantire la massima versatilità al programmatore che distribuisce le proprie applicazioni.
Sarà programmabile tramite un’intuitiva interfaccia grafica integrata in Studio.
Stay tuned!
Nella serie di post dedicata al pattern Model-View-ViewModel in WPF 4 con Visual Basic 2010, abbiamo studiato due differenti scenari, osservando come in entrambi abbiamo utilizzato alcune classi dello stesso tipo. Per capirci, sia negli esempi su oggetti custom che in quello su Entity Framework abbiamo utilizzato le classi RelayCommand e ViewModelBase.
Questo ci può portare a fare la seguente considerazione: è conveniente creare un framework per MVVM riutilizzabile in tutti i nostri progetti. In questo post creeremo un framework che è il caso di definire “light”, nel senso che conterrà i componenti riutilizzabili più comuni, anche se MVVM a volte consente (e richiede) l’utilizzo di altre tipologie di oggetti.
Personalmente ho appena pubblicato un nuovo progetto su CodePlex, chiamato Neptune MVVM, che consiste in un framework MVVM “light” per WPF e scritto con VB 2010, che potete scaricare da qui e che potete prendere come esempio da seguire per il post odierno.
Creeremo dapprima la libreria di classi, quindi un template di progetto riutilizzabile.
Creazione della libreria
La prima operazione da fare è creare una nuova libreria di classi in Visual Basic 2010 e aggiungere i seguenti riferimenti agli assembly di WPF:
1. WindowsBase.dll
2. PresentationCore.dll
3. PresentationFramework.dll
Fatto questo si può tranquillamente rimuovere il file Class1.vb. Le classi che faranno parte del nostro Framework sono le seguenti:
1. ViewModelBase, la classe base per tutti i ViewModel che potrà essere ereditata in altri progetti per la definizione di ViewModel specifici
2. Messenger, la classe che offre un servizio di scambio messaggi e che agisce da Message broker. Come detto a suo tempo, tale classe è ripresa dal framework Ocean di Karl Shifflett.
3. RelayCommand, la classe che si occupa di smistare la logica di esecuzione dei comandi, anche nella sua versione generica
4. ServiceLocator, la classe che consente di registrare e ottenere le istanze di classi di servizio nel momento in cui abbiamo la necessità di implementare un service layer
La descrizione dettagliata delle citate classi la potete trovare nei post della serie su MVVM. Quindi al progetto bisogna aggiungere i 4 file di codice di cui, per comodità, riporto l’implementazione di seguito.
Questa è la classe ViewModelBase:
Imports System.ComponentModel
Public MustInherit Class ViewModelBase
Implements INotifyPropertyChanged
Dim myServiceLocator As New ServiceLocator
Public Event PropertyChanged(ByVal sender As Object,
ByVal e As System.ComponentModel.PropertyChangedEventArgs)_
Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(ByVal strPropertyName As String)
If Me.PropertyChangedEvent IsNot Nothing Then
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(strPropertyName))
End If
End Sub
Private privateThrowOnInvalidPropertyName As Boolean
Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean
Get
Return privateThrowOnInvalidPropertyName
End Get
Set(ByVal value As Boolean)
privateThrowOnInvalidPropertyName = value
End Set
End Property
<Conditional("DEBUG"), DebuggerStepThrough()> _
Public Sub VerifyPropertyName(ByVal propertyName As String)
' Verify that the property name matches a real,
' public, instance property on this object.
If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then
Dim msg As String = "Invalid property name: " & propertyName
If Me.ThrowOnInvalidPropertyName Then
Throw New Exception(msg)
Else
Debug.Fail(msg)
End If
End If
End Sub
Private privateDisplayName As String
Public Overridable Property DisplayName() As String
Get
Return privateDisplayName
End Get
Protected Set(ByVal value As String)
privateDisplayName = value
End Set
End Property
Public Function ServiceLocator() As ServiceLocator
Return Me.myServiceLocator
End Function
Public Function GetService(Of T)() As T
Return myServiceLocator.GetService(Of T)()
End Function
End Class
La classe Messenger:
Imports System.Reflection
''' <summary>
''' Provides loosely-coupled messaging between
''' various colleague objects. All references to objects
''' are stored weakly, to prevent memory leaks.
''' </summary>
Public Class Messenger
#Region "Constructor"
Public Sub New()
End Sub
#End Region
#Region "Register"
''' <summary>
''' Registers a callback method to be invoked when a specific message is broadcasted.
''' </summary>
''' <param name="message">The message to register for.</param>
''' <param name="callback">The callback to be called when this message is broadcasted.</param>
Public Sub Register(ByVal message As String, ByVal callback As [Delegate])
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
If callback Is Nothing Then
Throw New ArgumentNullException("callback")
End If
Dim parameters As ParameterInfo() = callback.Method.GetParameters()
If parameters IsNot Nothing AndAlso parameters.Length > 1 Then
Throw New InvalidOperationException("The registered delegate can have no more than one parameter.")
End If
Dim parameterType As Type = If((parameters Is Nothing OrElse parameters.Length = 0), Nothing, parameters(0).ParameterType)
_messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType)
End Sub
#End Region
#Region "NotifyColleagues"
''' <summary>
''' Notifies all registered parties that a message is being broadcasted.
''' </summary>
''' <param name="message">The message to broadcast.</param>
Public Sub NotifyColleagues(ByVal message As String)
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
Dim actions = _messageToActionsMap.GetActions(message)
If actions IsNot Nothing Then
actions.ForEach(Function(action) action.DynamicInvoke())
End If
End Sub
''' <summary>
''' Notifies all registered parties that a message is being broadcasted.
''' </summary>
''' <param name="message">The message to broadcast</param>
''' <param name="parameter">The parameter to pass together with the message</param>
Public Sub NotifyColleagues(ByVal message As String, ByVal parameter As Object)
If String.IsNullOrEmpty(message) Then
Throw New ArgumentException("'message' cannot be null or empty.")
End If
Dim actions = _messageToActionsMap.GetActions(message)
If actions IsNot Nothing Then
actions.ForEach(Function(action) action.DynamicInvoke(parameter))
End If
End Sub
#End Region
#Region "MessageToActionsMap [nested class]"
''' <summary>
''' This class is an implementation detail of the Messenger class.
''' </summary>
Private Class MessageToActionsMap
' Stores a hash where the key is the message and the value is the list of callbacks to invoke.
ReadOnly _map As New Dictionary(Of String, List(Of WeakAction))()
Friend Sub New()
End Sub
''' <summary>
''' Adds an action to the list.
''' </summary>
''' <param name="message">The message to register.</param>
''' <param name="target">The target object to invoke, or null.</param>
''' <param name="method">The method to invoke.</param>
''' <param name="actionType">The type of the Action delegate.</param>
Friend Sub AddAction(ByVal message As String, ByVal target As Object, ByVal method As MethodInfo, ByVal actionType As Type)
If message Is Nothing Then
Throw New ArgumentNullException("message")
End If
If method Is Nothing Then
Throw New ArgumentNullException("method")
End If
SyncLock _map
If Not _map.ContainsKey(message) Then
_map(message) = New List(Of WeakAction)()
End If
_map(message).Add(New WeakAction(target, method, actionType))
End SyncLock
End Sub
''' <summary>
''' Gets the list of actions to be invoked for the specified message
''' </summary>
''' <param name="message">The message to get the actions for</param>
''' <returns>Returns a list of actions that are registered to the specified message</returns>
Friend Function GetActions(ByVal message As String) As List(Of [Delegate])
If message Is Nothing Then
Throw New ArgumentNullException("message")
End If
Dim actions As List(Of [Delegate])
SyncLock _map
If Not _map.ContainsKey(message) Then
Return Nothing
End If
Dim weakActions As List(Of WeakAction) = _map(message)
actions = New List(Of [Delegate])(weakActions.Count)
For i As Integer = weakActions.Count - 1 To -1 + 1 Step -1
Dim weakAction As WeakAction = weakActions(i)
If weakAction Is Nothing Then
Continue For
End If
Dim action As [Delegate] = weakAction.CreateAction()
If action IsNot Nothing Then
actions.Add(action)
Else
' The target object is dead, so get rid of the weak action.
weakActions.Remove(weakAction)
End If
Next
' Delete the list from the map if it is now empty.
If weakActions.Count = 0 Then
_map.Remove(message)
End If
End SyncLock
Return actions
End Function
End Class
#End Region
#Region "WeakAction [nested class]"
''' <summary>
''' This class is an implementation detail of the MessageToActionsMap class.
''' </summary>
Private Class WeakAction
ReadOnly _delegateType As Type
ReadOnly _method As MethodInfo
ReadOnly _targetRef As WeakReference
''' <summary>
''' Constructs a WeakAction.
''' </summary>
''' <param name="target">The object on which the target method is invoked, or null if the method is static.</param>
''' <param name="method">The MethodInfo used to create the Action.</param>
''' <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>
Friend Sub New(ByVal target As Object, ByVal method As MethodInfo, ByVal parameterType As Type)
If target Is Nothing Then
_targetRef = Nothing
Else
_targetRef = New WeakReference(target)
End If
_method = method
If parameterType Is Nothing Then
_delegateType = GetType(Action)
Else
_delegateType = GetType(Action(Of )).MakeGenericType(parameterType)
End If
End Sub
''' <summary>
''' Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.
''' </summary>
Friend Function CreateAction() As [Delegate]
' Rehydrate into a real Action object, so that the method can be invoked.
If _targetRef Is Nothing Then
Return [Delegate].CreateDelegate(_delegateType, _method)
Else
Try
Dim target As Object = _targetRef.Target
If target IsNot Nothing Then
Return [Delegate].CreateDelegate(_delegateType, target, _method)
End If
Catch
End Try
End If
Return Nothing
End Function
End Class
#End Region
#Region "Fields"
ReadOnly _messageToActionsMap As New MessageToActionsMap()
#End Region
End Class
La classe RelayCommand/RelayCommand(Of T):
Imports System.Windows.Input
Public Class RelayCommand
Implements ICommand
#Region "Fields"
Private ReadOnly _execute As Action(Of Object)
Private ReadOnly _canExecute As Predicate(Of Object)
#End Region ' Fields
#Region "Constructors"
''' <summary>
''' Creates a new command that can always execute.
''' </summary>
''' <param name="execute">The execution logic.</param>
Public Sub New(ByVal execute As Action(Of Object))
Me.New(execute, Nothing)
End Sub
''' <summary>
''' Creates a new command.
''' </summary>
''' <param name="execute">The execution logic.</param>
''' <param name="canExecute">The execution status logic.</param>
Public Sub New(ByVal execute As Action(Of Object), ByVal canExecute As Predicate(Of Object))
If execute Is Nothing Then
Throw New ArgumentNullException("execute")
End If
_execute = execute
_canExecute = canExecute
End Sub
#End Region ' Constructors
#Region "ICommand Members"
<DebuggerStepThrough()> _
Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
Return If(_canExecute Is Nothing, True, _canExecute(parameter))
End Function
Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
AddHandler(ByVal value As EventHandler)
AddHandler CommandManager.RequerySuggested, value
End AddHandler
RemoveHandler(ByVal value As EventHandler)
RemoveHandler CommandManager.RequerySuggested, value
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
End RaiseEvent
End Event
Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
_execute(parameter)
End Sub
#End Region ' ICommand Members
End Class
Public Class RelayCommand(Of T)
Implements ICommand
Private ReadOnly _execute As Action(Of T)
Private ReadOnly _canExecute As Predicate(Of T)
''' <summary>
''' Creates a new command that can always execute.
''' </summary>
''' <param name="execute">The execution logic.</param>
Public Sub New(ByVal execute As Action(Of T))
Me.New(execute, Nothing)
End Sub
''' <summary>
''' Creates a new command.
''' </summary>
''' <param name="execute">The execution logic.</param>
''' <param name="canExecute">The execution status logic.</param>
Public Sub New(ByVal execute As Action(Of T), ByVal canExecute As Predicate(Of T))
If execute Is Nothing Then
Throw New ArgumentNullException("execute")
End If
_execute = execute
_canExecute = canExecute
End Sub
<DebuggerStepThrough()> _
Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
Return If(_canExecute Is Nothing, True, _canExecute(CType(parameter, T)))
End Function
Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
AddHandler(ByVal value As EventHandler)
AddHandler CommandManager.RequerySuggested, value
End AddHandler
RemoveHandler(ByVal value As EventHandler)
RemoveHandler CommandManager.RequerySuggested, value
End RemoveHandler
RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
End RaiseEvent
End Event
Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
_execute(CType(parameter, T))
End Sub
End Class
La classe ServiceLocator:
Public Class ServiceLocator
Implements IServiceProvider
Private services As New Dictionary(Of Type, Object)()
Public Function GetService(Of T)() As T
Return CType(GetService(GetType(T)), T)
End Function
Public Function RegisterService(Of T)(ByVal service As T, ByVal overwriteIfExists As Boolean) As Boolean
SyncLock services
If Not services.ContainsKey(GetType(T)) Then
services.Add(GetType(T), service)
Return True
ElseIf overwriteIfExists Then
services(GetType(T)) = service
Return True
End If
End SyncLock
Return False
End Function
Public Function RegisterService(Of T)(ByVal service As T) As Boolean
Return RegisterService(Of T)(service, True)
End Function
Public Function GetService(ByVal serviceType As Type) As Object Implements IServiceProvider.GetService
SyncLock services
If services.ContainsKey(serviceType) Then
Return services(serviceType)
End If
End SyncLock
Return Nothing
End Function
End Class
A questo punto possiamo compilare il nostro progetto per avere una dll funzionante e che possiamo riutilizzare nei nostri progetti WPF senza dover tutte le volte riscrivere o copiare da altre parti il codice.
Per semplificarci la vita, però, possiamo anche creare un template di progetto riutilizzabile che ogni volta definisca gli elementi principali e referenzi automaticamente la nostra libreria.
Creazione di un template di progetto
Creiamo in primo luogo un nuovo progetto WPF con Visual Basic 2010. Per prima cosa aggiungiamo un riferimento alla libreria di classi appena creata. Se il progetto lo aggiungete alla soluzione contenente anche il progetto della libreria, fate attenzione ad aggiungere il riferimento alla dll e non al progetto, altrimenti il template punterà al progetto e non alla dll. Quindi creiamo le seguenti cartelle di progetto, che in realtà non riempiremo ma che saranno utili quando svilupperemo i progetti creati attraverso il template:
1. Models
2. ViewModels
3. Services
4. Views
Nella cartella Views spostiamo il file MainWindow.xaml, ricordando di andare in My Project e aggiornare la proprietà Startup URI. Per completare l’opera, nel file Application.xaml.vb definiamo l’istanza della classe Messenger:
Shared ReadOnly _messenger As New Messenger()
Friend Shared ReadOnly Property Msn As Messenger
Get
Return _messenger
End Get
End Property
Private Sub Application_DispatcherUnhandledException(ByVal sender As System.Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
MessageBox.Show(e.Exception.Message)
e.Handled = True
End Sub
Questo codice richiederà una direttiva Imports che punta al namespace che avete scelto per la libreria, nel caso del mio progetto Neptune il namespace è DelSole.Neptune. Ora possiamo esportare il template. Selezioniamo File|Export template e nella prima dialog assicuriamoci di selezionare Project Template nella prima combo e il nome di quest’ultimo progetto nella seconda. Andando avanti, imposteremo le proprietà del template in questo modo:
Al termine il template di progetto sarà disponibili tra quelli installati con Visual Studio, come si vede dalla seguente figura:
Conclusioni
In questo post abbiamo visto come sia possibile creare una libreria riutilizzabile per il pattern MVVM in WPF, con VB 2010 sebbene, come detto, questo sia in forma molto “light”. Il codice è disponibile su CodePlex, nella pagina della mia libreria Neptune.
Alessandro
A conclusione della serie introduttiva di post sul pattern MVVM in applicazioni WPF 4 con Visual Basic 2010, credo possa essere utile avere un elenco completo dei 10 post, per una più facile consultazione. Li riporto di seguito:
"WPF: Introduzione al pattern Model-View-ViewModel per sviluppatori Visual Basic 2010"
Parte 1 (introduzione)
Parte 2 (logica del commanding)
Parte 3 (visualizzazione a dettagli + RelayCommand generica)
Parte 4 (validazione dei dati)
(Dalla parte 5 in poi, MVVM nei confronti di ADO.NET Entity Framework)
Parte 5 (creazione Entity Data Model + validazione dei dati)
Parte 6 (Message Broker e commanding)
Parte 7 (strato di servizi)
Parte 8 (definizione ViewModels)
Parte 9 (refactoring + unit testing)
Parte 10 (definizione Views, UI e download codice)
Seguiranno altri post in cui prenderò a base l'applicazione di esempio creata nella serie appena riepilogata, ad esempio tra breve tempo parlerò di come cambiare il data store senza influenzare i ViewModel e di come aggiungere dati a design-time. Vi invito ancora a lasciare i vostri commenti/suggerimenti.
Alessandro
Sul sito dell'altra nostra community, WPF Tips & Tricks dedicata a WPF/Silverlight/Windows Phone, abbiamo recentemente pubblicato dei nuovi articoli che spero possano essere gradevoli letture da fare anche sotto l'ombrellone, visto che siamo in pieno periodo di vacanze estive :-)
Ve li riassumo:
Le novità di WPF 4 con Visual Studio 2010 (a cura di Alessandro Del Sole)
Introduzione a Windows Phone 7 con Visual Studio 2010 (a cura di Alessandro Del Sole)
LINQ to XML per creare codice XAML con Visual Basic 2010 (a cura di Renato Marzaro)
WPF: Implementare l'interfaccia IValueConverter (a cura di Alessandro Del Sole)
Abbiamo anche pubblicato un mio nuovo screencast di introduzione a Windows Phone 7 con Visual Studio 2010, disponibile nell'area Video del portale. Buona lettura e buone ferie!
Alessandro
Finalmente con questo post concludiamo la serie di articoli introduttivi al pattern Model-View-ViewModel in WPF con Visual Basic 2010, in particolare nei confronti di ADO.NET Entity Framework. Vedremo quindi come costruire l'interfaccia grafica e come collegarla, in data-binding, ai ViewModel. Anche se concludiamo questa serie introduttiva, non vuol dire che non tornerò sull'argomento. Anzi, MVVM sarà sicuramente un argomento centrale per questo blog ma per tenere ben distinti gli argomenti, concludiamo qui l'applicazione dimostrativa. Al termine del post trovate il link per scaricare il progetto sorgente completo, dall'area Download di VB T&T.
Andrò abbastanza spedito in questo post, perché il codice è tanto e i concetti sono stati illustrati nel corso della serie. Niente di trascendentale, cose che già abbiamo imparato.
L'ErrorTemplate
Come ricorderete, nel primo post di questa parte dedicata a MVVM con Entity Framework abbiamo implementato regole di validazione dei dati nella classe Order, provvista inoltre dell'interfaccia IDataErrorInfo. Affinché l'interfaccia grafica sia in grado di riflettere gli errori di validazione, è necessario definire un ErrorTemplate per i controlli di tipo TextBox (che utilizzeremo nella View). Il codice di cui faccio utilizzo è stato già illustrato nella parte 4 della serie, ad ogni buon conto lo riporto di seguito e va inserito all'interno del file Application.xaml:
<Application x:Class="Application"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Views\MainWindow.xaml">
<Application.Resources>
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Margin" Value="5"/>
</Style>
<!--Template per validazione controlli
Mostrerà un bordo rosso e un messaggio
quando la validazione dell'ordine fallisce
-->
<Style TargetType="Control" x:Key="myErrorTemplate">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red"
FontSize="14pt"
FontWeight="ExtraBold">*
</TextBlock>
<Border BorderBrush="Red" BorderThickness="4">
<AdornedElementPlaceholder Name="myControl"/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}"/>
<Style TargetType="Label">
<Setter Property="Foreground" Value="White"/>
</Style>
</Application.Resources>
</Application>
Il template così definito mostrerà un bordo rosso con asterisco intorno alle TextBox che conterranno dati non validi, finché i dati stessi non saranno corretti.
La View principale
Per come abbiamo visto la figura nel primo post per MVVM/Entity Framework, l'interfaccia dovrà essere così composta:
- una Grid principale che contiene:
- una ListBox per l'elenco clienti
- una Grid che conterrà i controlli per visualizzare le informazioni sul singolo ordine
- uno StackPanel che conterrà i pulsanti
Quindi, per prima cosa definiamo la Window come segue, specificando i riferimenti all'assembly dell'applicazione e aggiungendo anche un background a gradiente un po' carino:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Northwind Traders Orders Management" Height="508" Width="876" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:my="clr-namespace:adsMVVM_EntityFramework_Complete"
WindowStartupLocation="CenterScreen">
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Window.Background>
Chiaramente il nome dell'assembly varierà sulla vostra macchina, a seconda di come avete chiamato il progetto. Suddividiamo ora in tre parti la griglia principale, assegnando un nome che ci sarà utile nel file di code-behind:
<Grid Name="MainGrid" ShowGridLines="False" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
Ora dichiariamo la ListBox per l'elenco clienti. Fate attenzione al binding:
<ListBox Name="ListBox1" Grid.Column="0" ItemsSource="{Binding Path=CustomerViewSource.View}" DisplayMemberPath="CompanyName"
SelectedItem="{Binding Path=Selection, Mode=TwoWay}" />
Avviene questo: il controllo è in binding (ItemsSource) con la proprietà CustomersViewSource.View dell'oggetto che verrà assegnato al DataContext della Grid principale (ossia il ViewModel). Quindi questa volta i controlli sono in binding con le view delle CollectionViewSource/ListCollectionView invece che con un ObservableCollection. DisplayMemberPath consente di specificare l'elemento della collezione che deve essere mostrato. Notate anche come l'elemento selezionato della ListBox sia in binding con la proprietà Selection del ViewModel, in lettura/scrittura. Ora è la volta della Grid centrale che contiene TextBox e DatePicker (questi per la visualizzazione di date) per rappresentare un singolo ordine. Notate come ogni controllo di questo tipo sia in binding con la corrispondente proprietà proveniente dalla sorgente associata (ossia la classe Order), ma soprattutto notate come la Grid si alimenti dall'oggetto CustomerOrdersView che viene esposto dal ViewModel che verrà associato tra poco alla View:
<Grid Grid.Column="1" Name="Grid1" DataContext="{Binding Path=CustomerOrdersView}" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="Order ID:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3" Name="OrderIDTextBox"
Text="{Binding Path=OrderID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Customer ID:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="3" Name="CustomerIDTextBox"
Text="{Binding Path=CustomerID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Employee ID:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Height="23" HorizontalAlignment="Left" Margin="3" Name="EmployeeIDTextBox"
Text="{Binding Path=EmployeeID, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Order Date:" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<DatePicker Grid.Column="1" Grid.Row="3" Height="25" HorizontalAlignment="Left" Margin="3" Name="OrderDateDatePicker"
SelectedDate="{Binding Path=OrderDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
<Label Content="Required Date:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<DatePicker Grid.Column="1" Grid.Row="4" Height="25" HorizontalAlignment="Left" Margin="3" Name="RequiredDateDatePicker"
SelectedDate="{Binding Path=RequiredDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
<Label Content="Shipped Date:" Grid.Column="0" Grid.Row="5" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<DatePicker Grid.Column="1" Grid.Row="5" Height="25" HorizontalAlignment="Left" Margin="3" Name="ShippedDateDatePicker"
SelectedDate="{Binding Path=ShippedDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="115" />
<Label Content="Ship Via:" Grid.Column="0" Grid.Row="6" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="6" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipViaTextBox"
Text="{Binding Path=ShipVia, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Freight:" Grid.Column="0" Grid.Row="7" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="7" Height="23" HorizontalAlignment="Left" Margin="3" Name="FreightTextBox"
Text="{Binding Path=Freight, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship Name:" Grid.Column="0" Grid.Row="8" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="8" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipNameTextBox"
Text="{Binding Path=ShipName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship Address:" Grid.Column="0" Grid.Row="9" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="9" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipAddressTextBox"
Text="{Binding Path=ShipAddress, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship City:" Grid.Column="0" Grid.Row="10" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="10" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipCityTextBox"
Text="{Binding Path=ShipCity, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship Region:" Grid.Column="0" Grid.Row="11" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="11" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipRegionTextBox"
Text="{Binding Path=ShipRegion, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship Postal Code:" Grid.Column="0" Grid.Row="12" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="12" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipPostalCodeTextBox"
Text="{Binding Path=ShipPostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" Width="120" />
<Label Content="Ship Country:" Grid.Column="0" Grid.Row="13" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="13" Height="23" HorizontalAlignment="Left" Margin="3" Name="ShipCountryTextBox"
Text="{Binding Path=ShipCountry, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true, ValidatesOnDataErrors=True}" VerticalAlignment="Center" Width="120" />
</Grid>
Fate attenzione al fatto che è importante impostare il markup ValidatesOnDataErrors = True nel binding della TextBox relativa alla proprietà ShipCountry, che è quella per la quale abbiamo previsto regole di validazione lato Model. Per completare l'interfaccia della finestra principale ci manca solo la dichiarazione dei pulsanti e il relativo binding dei comandi. Ecco il codice:
<StackPanel Name="Stack2" Orientation="Vertical" Grid.Column="2" >
<Button Content="Save changes"
Command="{Binding SaveCommand}"
Name="SaveButton" Style="{StaticResource ButtonStyle}" />
<Button Content="Next"
Name="NextButton"
Command="{Binding NextCommand}"
Style="{StaticResource ButtonStyle}" />
<Button Content="Previous" Command="{Binding PreviousCommand}"
Name="PrevButton" Style="{StaticResource ButtonStyle}" />
<Button Content="Delete order"
Name="DeleteButton"
Command="{Binding DeleteCommand}"
Style="{StaticResource ButtonStyle}" />
<Button Content="New order"
Command="{Binding Path=InsertCommand}"
Name="NewButton"
Style="{StaticResource ButtonStyle}" />
<Button Content="View details"
Command="{Binding ViewDetailsCommand}"
Name="Button1"
Style="{StaticResource ButtonStyle}" />
</StackPanel>
</Grid>
</Window>
Ricordate quindi come non si gestisca l'evento Click, ma si associa la proprietà Command a una delle proprietà di tipo ICommand che abbiamo predisposto nel ViewModel. Passiamo al code-behind in Visual Basic per rendere operativa la finestra principale.
No code? No party!
Nei primi post di questa serie, abbiamo visto che nel code-behind della finestra principale, ossia la prima View, c'era solamente codice che istanziava il ViewModel e lo assegnava al DataContext della Window stessa. Ora, il principio è questo: la View può contenere altro codice, purché sia codice relativo alla gestione esclusiva dell'interfaccia grafica e che, quindi, non acceda a dati o cose simili. Nel nostro caso specifico, il code-behind della Window conterrà senza dubbio il codice che istanzia e assegna il ViewModel, ma in più si occuperà di registrare il messaggio che, una volta emesso, consentirà di visualizzare la finestra relativa agli Order Details, cosa che dobbiamo fare utilizzando il metodo Register della classe Messenger. In sostanza, questo è il code-behind:
Class MainWindow
Private Sub MainWindow_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
Dim startupViewModel As New OrdersViewModel
Me.MainGrid.DataContext = startupViewModel
Application.Msn.Register(Application.VIEW_DETAILS_EXECUTE, Sub()
Try
'Ottiene l'istanza dell'Order corrente e ne passa l'ID al costruttore
'della finestra OrderDetailsView
Dim odv As New OrderDetailsView(CType(startupViewModel.CustomerOrdersViewSource.View.CurrentItem, Order).OrderID)
odv.ShowDialog()
odv = Nothing
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub)
End Sub
End Class
Queste sono le considerazioni fondamentali:
- l'istanza del ViewModel viene assegnata non al DataContext della Window ma a quello della prima Grid. Questo è un caso particolare, poiché, per linea gerarchica, i vari controlli andranno a popolarsi da questo primo contenitore.
- il metodo Register riceve, come argomenti, il messaggio da registrare e l'azione da intraprendere nel momento il cui il messaggio è emesso (da parte del ViewModel). In questo caso il messaggio è stato definito come costante a livello di applicazione, mentre l'azione da intraprendere è stata definita attraverso una statement lambda invece che puntare a un delegate separato attraverso AddressOf (pratica che comunque andava bene ugualmente).
- Con riferimento al secondo argomento del metodo Register, la lambda ottiene l'istanza dell'Order selezionato e passa il suo identificativo al costruttore di una finestra chiamata OrderDetailsView, che creeremo tra breve, quindi istanzia e mostra tale finestra. Trattandosi di codice legato alla UI, si può tranquillamente scrivere a livello di View.
- Per quanto riguarda lo scambio di messaggi: la View registra il messaggio e l'applicazione rimarrà in ascolto -> il ViewModel emette il messaggio quando è invocato il command desiderato -> la View intercetta il messaggio e istanzia la nuova finestra
Completata la finestra principale, passiamo alla seconda View.
Una View per gli Order Details
Nella nostra applicazione di esempio gli Order Details verranno mostrati all'interno di una DataGrid, in una nuova finestra; per semplicità non implementeremo altri controlli ma nessuno vi vieta di farlo per conto vostro, una volta capita la logica. Quindi, alla cartella di progetto chiamata Views aggiungiamo una nuova Window chiamata OrderDetailsView.xaml. Quando pronti, potete facilitarvi il compito utilizzando il data-binding drag'n'drop, quindi trascinando dalla finestra Data Sources l'entità Order_Details sul designer della finestra. Questo abiliterà alcune funzioni di design-time per il lavoro sui dati, ad ogni buon conto lo XAML che dovremo scrivere è il seguente (comprensivo delle espressioni generate da Visual Studio 2010 a seguito di drag'n'drop, utili a design time):
<Window x:Class="OrderDetailsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:adsMVVM_EntityFramework_Complete"
xmlns:dal="clr-namespace:DAL;assembly=DAL" ShowInTaskbar="True" WindowStartupLocation="CenterScreen"
Title="OrderDetailsView" Height="300" Width="370" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:my1="clr-namespace:DAL;assembly=DAL">
<Window.Resources>
<CollectionViewSource x:Key="Order_DetailViewSource" Source="{Binding Path=Order_Details}" d:DesignSource="{d:DesignInstance dal:Order_Detail, CreateList=True}" />
</Window.Resources>
<Grid DataContext="{StaticResource Order_DetailViewSource}">
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding}" Name="Order_DetailsDataGrid"
RowDetailsVisibilityMode="VisibleWhenSelected">
<DataGrid.Columns>
<DataGridTextColumn x:Name="ProductIDColumn" Binding="{Binding Path=ProductID}" Header="Product ID" Width="SizeToHeader" />
<DataGridTextColumn x:Name="UnitPriceColumn" Binding="{Binding Path=UnitPrice, StringFormat=c}" Header="Unit Price" Width="SizeToHeader" />
<DataGridTextColumn x:Name="QuantityColumn" Binding="{Binding Path=Quantity}" Header="Quantity" Width="SizeToHeader" />
<DataGridTextColumn x:Name="DiscountColumn" Binding="{Binding Path=Discount, StringFormat=p}" Header="Discount" Width="SizeToHeader" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Potete quindi notare come sia possibile ricorrere a una CollectionViewSource per impostare, a design-time, il DataContext della nostra Grid. Tale sorgente punta alla proprietà Order_Details esposta dal ViewModel. A livello di code-behind, il discorso è simile alla View principale. Qui dobbiamo esplicitare il costruttore, prevedendo la ricezione, come parametro, dell'identificativo dell'ordine perché questo servirà al ViewModel per estrarre i dati relativi all'Order richiesto:
Public Class OrderDetailsView
Private _orderID As Integer
Private WithEvents OrderDetailsVM As OrderDetailsViewModel
Sub New(ByVal OrderID As Integer)
InitializeComponent()
' TODO: Complete member initialization
Me._orderID = OrderID
End Sub
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Me.Title = "Order details for order nr. " & Me._orderID.ToString
Me.OrderDetailsVM = New OrderDetailsViewModel(_orderID)
Me.DataContext = Me.OrderDetailsVM
Application.Msn.Register(Application.VIEW_DETAILS_CLOSE, Sub()
Me.Close()
End Sub)
End Sub
End Class
Essenzialmente qui il messaggio per la chiusura della finestra è implementato ma non utilizzato in modo effettivo, potete rimuoverlo, se preferite, oppure utilizzarlo agganciandolo a un pulsante di chiusura che punti al comando CloseCommand esposto dal ViewModel associato.
Premiamo F5, finalmente!
Al termine di tutto questo grande lavoro, possiamo finalmente avviare l'applicazione. Il tutto si presenterà più o meno così, con la finestra degli Order Details aperta:
Questo, invece, è ciò che otterremo nel caso di un errore di validazione:
Conclusioni e Download
Come ampiamente detto, potrebbero esserci delle imprecisioni nella serie di articoli ma spero che comunque sia risultata utile per iniziare ad approcciare MVVM, dal punto di vista di Visual Basic 2010, soprattutto in questa seconda parte dedicata ad Entity Framework perché la rete è veramente parca di esempi in merito. La discussione rimane chiaramente aperta e comunque tornerò presto sull'argomento, innanzitutto fornendo un post in cui sono riepilogate tutte le puntate della serie e poi vedendo come costruire un piccolo framework riutilizzabile. Il codice sorgente dell'applicazione completa può essere scaricata da questo indirizzo dell'area Download di VB T&T, ricordando che per poter utilizzare il codice dovete avere installato il database Northwind, reso disponibile sull'istanza di SQL Server (potete naturalmente cambiare la stringa di connessione se avete il file in locale, ad esempio).
Vi aspetto quindi con i prossimi post su MVVM!
Alessandro
Finora abbiamo scritto molto codice, nella serie di post che stiamo trattando relativa a MVVM in WPF con Visual Basic 2010. Ci sono sicuramente delle migliorie da fare e qualche imprecisione da correggere per cui è necessario fare un po' di refactoring prima di passare alla fase finale. Questo ci permetterà, fra l'altro, di scoprire un altro grosso beneficio del pattern MVVM, ossia l'utilizzo di unit tests nei confronti del nostro ViewModel. Partiamo dal refactoring riorganizzando alcune parti del nostro codice.
Refactoring, che passione!
L'attività di refactoring sarà concentrata essenzialmente sui due ViewModel. L'OrdersViewModel, in particolare, è quello che necessita di maggior attenzione. La prima cosa che vogliamo migliorare è l'utilizzo della classe RelayCommand: dal momento che abbiamo fornito un'implementazione generica, è conveniente utilizzare questa al posto di quella non generica. Andiamo quindi nel codice della classe OrdersViewModel e posizioniamoci nell'area che espone le proprietà di tipo ICommand. A titolo esemplificativo riporto il lavoro da eseguire sulla prima, che poi dovrete estendere a tutte le altre. Riscriviamole in questo modo, utilizzando RelayCommand(Of Order):
Public ReadOnly Property DeleteCommand() As ICommand
Get
If _cmdDeleteCommand Is Nothing Then
_cmdDeleteCommand = New RelayCommand(Of Order)(AddressOf DeleteExecute, AddressOf CanDeleteExecute)
End If
Return _cmdDeleteCommand
End Get
End Property
Chiaramente va poi modificata la firma dei relativi metodi a cui si punta tramite AddressOf, affinchè ricevano un parametro tipizzato invece che un Object. Quindi li riscriviamo così:
Private Function CanDeleteExecute(ByVal param As Order) As Boolean
If Me.CustomerOrdersView Is Nothing Then Return False
Return Me.CustomerOrdersView.CurrentPosition > -1
End Function
Private Sub DeleteExecute(ByVal param As Order)
Me.orderAccess.Delete(Me.CustomerOrdersView)
End Sub
Anche con riferimento ai metodi, dovrete applicare le modifiche a tutti i restanti. Andiamo poi nel gestore di evento chiamato _customersView_CurrentChanged. Estraiamo un nuovo metodo dal contenuto del gestore di evento, per migliorare l'organizzazione del codice e per favorire, se lo vorrete (e vi lascio liberi di provarlo :-)), la creazione di unit test. Il codice diventa quindi il seguente:
Private Sub _customersView_CurrentChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles _customersView.CurrentChanged
Me.UpdateOrdersViewAfterCustomerChanged(CType(Me.CustomersView.CurrentItem, Customer))
End Sub
Private Function UpdateOrdersViewAfterCustomerChanged(ByVal currentCustomer As Customer) As Boolean
Try
Me.Orders = orderAccess.GetAllOrders(currentCustomer.CustomerID)
Me.CustomerOrdersViewSource.Source = Me.Orders
Me.CustomerOrdersView = CType(Me.CustomerOrdersViewSource.View, ListCollectionView)
Return True
Catch ex As Exception
Return False
End Try
End Function
Sempre al fine di favorire la creazione di unit test da eseguire sul ViewModel, invece di invocare dall'interno del costruttore il metodo CustomerDataService.GetAllCustomers, facciamo fare tale invocazione a un nuovo metodo interno al ViewModel che sarà possibile testare e il suo risultato sarà assegnato agli oggetti appropriati. Quindi, per prima cosa spostiamo a livello di classe la seguente dichiarazione:
Private dataAccess As ICustomerDataService
Fatto questo, scriviamo il seguente metodo che restituisce il risultato della chiamata:
Private Function GetAllCustomers() As IQueryable(Of Customer)
Return Me.dataAccess.GetAllCustomers
End Function
All'interno del costruttore, poi, dobbiamo sostituire questo codice:
Dim dataAccess = GetService(Of ICustomerDataService)()
Me.orderAccess = GetService(Of IOrderDataService)()
'Cicla l'elenco dei clienti ottenuto e lo aggiunge
'alla collezione
For Each element In dataAccess.GetAllCustomers
Me._customers.Add(element)
Next
con il seguente, che sfrutta il precedente metodo:
Me.dataAccess = GetService(Of ICustomerDataService)()
Me.orderAccess = GetService(Of IOrderDataService)()
'Cicla l'elenco dei clienti ottenuto e lo aggiunge
'alla collezione
For Each element In Me.GetAllCustomers
Me._customers.Add(element)
Next
Niente paura se vi siete persi qualche passaggio, nel prossimo post ci sarà il codice completo da scaricare. Per quanto riguarda il primo ViewModel, il refactoring è completo. Per quanto riguarda l'altro, OrderDetailsViewModel, c'è da applicare l'utilizzo di RelayCommand(Of Order_Detail). I passaggi da eseguire sono gli stessi mostrati all'inizio di questo paragrafo, per cui ve li lascio come esercizio :-)
Scrittura ed esecuzione di unit test
Come sapete, gli unit test ci consentono di testare il funzionamento di porzioni del nostro codice, in modo astratto dal contesto dell'applicazione. Significa quindi che uno unit test verifica il funzionamento del nostro codice senza dover lanciare l'applicazione, in modo totalmente separato, attraverso apposita strumentazione di Visual Studio 2010. Quella di scrivere unit test è sicuramente un'ottima pratica di programmazione e il pattern MVVM sembra fatto apposta per favorire questo tipo di approccio. Provate infatti a pensare a come potreste eseguire degli unit test nei confronti di codice associato alle View, quindi all'interfaccia grafica. Si, forse fattibile, ma comunque legato al contesto della UI. Ora, invece, pensate al metodo GetAllCustomers implementato precedentemente. E' nel ViewModel, quindi non sta nel lato dell'interfaccia, ed è in grado di recuperare dei dati. Quindi è possibile testarne il funzionamento scrivendo uno unit test che verifichi l'effettivo caricamento dei dati, il tutto senza interferire con l'interfaccia. Per quanto molto semplice ed essenziale, quindi assolutamente non strabiliante, questo esempio calza a pennello. Detto questo, andiamo nel nostro OrdersViewModel e facciamo click destro sul metodo GetAllCustomers. Dal menu contestuale selezioniamo la voce Create Unit Test. A questo punto comparirà la dialog illustrata in figura, dalla quale possiamo selezionare ulteriori metodi e, soprattutto, specificare un nome per il progetto di test:
A questo punto Visual Studio 2010 genera il progetto di test, sulle cui caratteristiche non mi soffermo (trovate spiegazioni appropriate in questo mio webcast su Microsoft Be-IT), mentre la prima cosa che dobbiamo fare è copiare il file App.config dal progetto primario, atteso che questo contiene le informazioni di connessione al database, necessarie anche in questo contesto. Successivamente, nel file OrdersViewModelTest.vb andiamo a reperire il metodo chiamato GetAllCustomersTest. Lo scopo del nostro unit test è quello di verificare che il metodo GetAllCustomers carichi effettivamente dei dati dal database e che quindi restituisca un valore non nullo. Riscriviamo quindi il metodo nel modo seguente:
<TestMethod(), _
DeploymentItem("adsMVVM_EntityFramework_Complete.exe")> _
Public Sub GetAllCustomersTest()
Dim target As OrdersViewModel_Accessor = New OrdersViewModel_Accessor() ' TODO: Initialize to an appropriate value
Dim actual As IQueryable(Of Customer)
actual = target.GetAllCustomers
Assert.IsNotNull(actual)
End Sub
Ora, utilizzando l'apposita strumentazione di Visual Studio (o semplicemente facendo click destro nel file di codice e cliccando la voce Run Tests del menu contestuale) avviamo l'esecuzione del test. Come si può vedere dalla seguente figura, il test viene superato:
Questo perché effettivamente il metodo GetAllCustomers ha caricato dei dati e quindi restituisce qualcosa che non è null, rispondendo quindi ai requisiti dell'Assert. Come detto, questo è un esempio banale. Tuttavia dovrebbe iniziare a far capire il vantaggio offerto dal pattern MVVM in questi contesti, ossia favorire l'esecuzione del test di porzioni di codice grazie al livello di astrazione raggiunto dal codice stesso, grazie all'intervento del ViewModel.
Fine della parte
Siamo giunti al termine anche della nona parte. Abbiamo migliorato il nostro codice e scritto unit test, vedendo il livello di astrazione che MVVM consente di raggiungere. Nel prossimo post completeremo finalmente il nostro lavoro, predisponendo le View, ossia l'interfaccia grafica. Vedremo ancora qualche scambio di messaggi, molto data-binding e scopriremo il concetto secondo cui la View <> "no code" ma View = "UI code".
Alessandro
UPDATED!
Riprendiamo il nostro percorso nello studio del pattern MVVM nei confronti di applicazioni WPF scritte con VB 2010. La scorsa volta ci siamo salutati dopo aver implementato uno strato di servizi, in questo post invece ci occuperemo dei ViewModel. Nello specifico ci occuperemo di trovare un posto all'istanza della classe Messenger nonché di scrivere tutti i ViewModel, per cui sarà un lavoretto un po' lungo, soprattutto in termini di codice dato che i concetti sul ViewModel sono già stati illustrati in questo precedente post.
Messaggiamo!
In tutti i tutorial che ho trovato, e quindi studiato, la dichiarazione della classe Messenger viene messa a livello di applicazione come proprietà di sola lettura. I metodi di tale classe ricevono degli argomenti stringa che rappresentano i messaggi da scambiare tra oggetti "colleghi" e che poi verranno intercettati. Invece di scrivere tutte le volte delle stringhe, si possono definire delle costanti che contengono i messaggi. Quindi la classe Application diventa anche il luogo in cui definire tali costanti. A tal proposito, a noi interessa registrare e intercettare due messaggi: uno per aprire la finestra degli Order Details e uno relativo alla chiusura di tale finestra. Ciò premesso, ecco il codice che ci occorre all'interno del file Application.xaml.vb:
Class Application
' Application-level events, such as Startup, Exit, and DispatcherUnhandledException
' can be handled in this file.
Friend Const VIEW_DETAILS_EXECUTE As String = "ViewDetailsExecute"
Friend Const VIEW_DETAILS_CLOSE As String = "ViewDetailsClose"
Shared ReadOnly _messenger As New Messenger()
Friend Shared ReadOnly Property Msn As Messenger
Get
Return _messenger
End Get
End Property
Private Sub Application_DispatcherUnhandledException(ByVal sender As System.Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
MessageBox.Show(e.Exception.Message)
e.Handled = True
End Sub
End Class
Troveremo anche nel corso di questo post un esempio di utilizzo della classe Messenger. Fatto questo, passiamo ai ViewModel.
Ogni View il suo ViewModel, ogni ViewModel la sua View
Partiamo da un concetto: ogni View, ossia ogni finestra o user control atto alla presentazione dei dati, ha un suo ViewModel col quale dialoga. Nella nostra applicazione abbiamo due finestre: quella principale e quella per la visualizzazione dei dettagli/ordine. Di conseguenza dobbiamo implementare due ViewModel. Come già facemmo precedentemente, implementeremo una classe chiamata ViewModelBase che, tramite ereditarietà, propagherà alcuni membri di interesse comune per tutti i ViewModel. Ciò premesso, nella cartella del progetto che abbiamo chiamato ViewModels aggiungiamo una nuova classe chiamata ViewModelBase.vb. Questa versione della classe ViewModelBase è estesa rispetto alla precedente, con alcuni membri che consentono la validazione di proprietà. Sebbene non faremo un uso particolare di queste feature, vi potrebbe in futuro servire. Inoltre, cosa fondamentale, introduciamo i metodi ServiceLocator e GetService che restituiscono, rispettivamente, l'istanza della classe ServiceLocator che smista le richieste sui servizi e l'istanza della classe di servizio specificata. Di questo faremo uso nei ViewModel derivati. Pertanto, la classe ora si presenta così:
Imports System.ComponentModel
Public MustInherit Class ViewModelBase
Implements INotifyPropertyChanged
Dim myServiceLocator As New ServiceLocator
Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(ByVal strPropertyName As String)
If Me.PropertyChangedEvent IsNot Nothing Then
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(strPropertyName))
End If
End Sub
Private privateThrowOnInvalidPropertyName As Boolean
Protected Overridable Property ThrowOnInvalidPropertyName() As Boolean
Get
Return privateThrowOnInvalidPropertyName
End Get
Set(ByVal value As Boolean)
privateThrowOnInvalidPropertyName = value
End Set
End Property
<Conditional("DEBUG"), DebuggerStepThrough()> _
Public Sub VerifyPropertyName(ByVal propertyName As String)
' Verify that the property name matches a real,
' public, instance property on this object.
If TypeDescriptor.GetProperties(Me)(propertyName) Is Nothing Then
Dim msg As String = "Invalid property name: " & propertyName
If Me.ThrowOnInvalidPropertyName Then
Throw New Exception(msg)
Else
Debug.Fail(msg)
End If
End If
End Sub
Private privateDisplayName As String
Public Overridable Property DisplayName() As String
Get
Return privateDisplayName
End Get
Protected Set(ByVal value As String)
privateDisplayName = value
End Set
End Property
Public Function ServiceLocator() As ServiceLocator
Return Me.myServiceLocator
End Function
Public Function GetService(Of T)() As T
Return myServiceLocator.GetService(Of T)()
End Function
End Class
Niente di complicato quindi. E, infatti, il bello viene adesso.
Un ViewModel per la relazione Customer/Orders
Nella finestra principale dell'applicazione vogliamo visualizzare un elenco di clienti e, alla selezione del cliente, mostrare i dettagli degli ordini associati al cliente stesso, offrendo anche funzionalità di spostamento tra dati nonché di inserimento ed eliminazione. Quindi dobbiamo creare un ViewModel per esporre alla View i dati richiesti nonché i membri necessari all'esecuzione di varie operazioni. Il ViewModel implementerà anche IDataErrorInfo (tecnica descritta qui) al fine di inviare alla View gli errori di validazione sollevati dal Model. Ora il punto è questo: generalmente, noi esponiamo dati attraverso proprietà di tipo ObservableCollection; questo è corretto, perché tale collezione offre pieno supporto al data-binding in WPF. Come detto, però, a noi interessa avere dei metodi che ci permettano di sfogliare e modificare i dati. Per fare questo, possiamo utilizzare oggetti di tipo ICollectionView ed esporre alle View oggetti di tipo ListCollectionView. Questo particolare oggetto, infatti, offre funzionalità avanzate di lavoro sui dati ed è "bindabile". Detto questo, dovremo implementare in sostanza:
-
Commands per eseguire le operazioni
-
IDataErrorInfo per la validazione
-
Istanze delle classi di servizio che ci consentano di accedere al db
-
Proprietà relative ai Model che espongano i dati alla View
-
Oggetti di tipo ListCollectionView che, in binding, permettano di gestire i dati attraverso la View
-
Proprietà di tipo ObservableCollection che restituiscano i dati. Sebbene questo non sarà utilizzato, può essere utile per futuri e diversi utilizzi mentre i relativi backing field ci occorrono come supporto per le ListCollectionView
Quindi, dopo aver aggiunto un nuovo file di codice chiamato OrdersViewModel.vb al progetto, iniziamo con l'aggiungere i campi di supporto:
Public Class OrdersViewModel
Inherits ViewModelBase
Implements IDataErrorInfo
#Region " Declarations "
'Serie di comandi necessari
Private _cmdDeleteCommand As ICommand
Private _cmdInsertCommand As ICommand
Private _cmdNextCommand As ICommand
Private _cmdPreviousCommand As ICommand
Private _cmdSaveCommand As ICommand
Private _cmdViewDetails As ICommand
'Un singolo ordine
Private _objOrder As Order
'Rappresenta l'istanza del cliente selezionato
Private _selection As Customer
'Espone i dati nel modo classico
Private _orders As ObservableCollection(Of Order)
Private _customers As ObservableCollection(Of Customer)
'Un "ponte" tra View e dati
Private _customerViewSource As New CollectionViewSource
Private _customerOrdersViewSource As New CollectionViewSource
'Liste di oggetti modificabili, per clienti e ordini
Private WithEvents _customersView As ListCollectionView
Private WithEvents _customerOrdersView As ListCollectionView
'Otterrà l'istanza della classe di servizio
Private orderAccess As IOrderDataService
#End Region
A questo punto possiamo passare alle proprietà relative ai dati. Innanzitutto esporremo le CollectionView tramite delle proprietà e la stessa cosa sarà fatta per gli oggetti di tipo ListCollectionView; questo è importante per avere la base di dati su cui lavorare. Poi avremo una proprietà che rappresenta l'istanza del cliente selezionato e una che rappresenta l'istanza dell'ordine selezionato. Esporremo, per completezza, anche 2 proprietà di tipo ObservableCollection per customers e orders nel caso in cui un domani decidessimo di usare queste invece che le ListCollectionView. Alla fine del discorso, il codice delle proprietà è il seguente:
#Region " Properties "
Public ReadOnly Property CustomerViewSource As CollectionViewSource
Get
Return Me._customerViewSource
End Get
End Property
Public Property CustomerOrdersViewSource As CollectionViewSource
Get
Return Me._customerOrdersViewSource
End Get
Set(ByVal value As CollectionViewSource)
Me._customerOrdersViewSource = value
OnPropertyChanged("CustomerOrdersViewSource")
End Set
End Property
Public Property CustomersView As ListCollectionView
Get
Return Me._customersView
End Get
Set(ByVal value As ListCollectionView)
Me._customersView = value
OnPropertyChanged("CustomersView")
End Set
End Property
Public Property CustomerOrdersView As ListCollectionView
Get
Return Me._customerOrdersView
End Get
Set(ByVal value As ListCollectionView)
Me._customerOrdersView = value
OnPropertyChanged("CustomerOrdersView")
End Set
End Property
'Rappresenta l'istanza del cliente selezionato
Public Property Selection As Customer
Get
Return Me._selection
End Get
Set(ByVal value As Customer)
If value Is _selection Then
Return
End If
_selection = value
MyBase.OnPropertyChanged("Selection")
End Set
End Property
Public Property Customers As ObservableCollection(Of Customer)
Get
Return Me._customers
End Get
Set(ByVal value As ObservableCollection(Of Customer))
Me._customers = value
OnPropertyChanged("Customers")
End Set
End Property
Public Property Orders As ObservableCollection(Of Order)
Get
Return Me._orders
End Get
Set(ByVal value As ObservableCollection(Of Order))
Me._orders = value
OnPropertyChanged("Orders")
End Set
End Property
Public Property Order() As Order
Get
Return _objOrder
End Get
Set(ByVal Value As Order)
_objOrder = Value
OnPropertyChanged("Order")
End Set
End Property
Public Property Customer() As Customer
Get
Return _objOrder.Customer
End Get
Set(ByVal Value As Customer)
_objOrder.Customer = Value
OnPropertyChanged("Customer")
End Set
End Property
#End Region
Fatto questo abbiamo bisogno delle proprietà che espongano i Command. Niente di difficile, ne abbiamo parlato qui. Ecco il codice:
#Region " Command Properties "
Public ReadOnly Property DeleteCommand() As ICommand
Get
If _cmdDeleteCommand Is Nothing Then
_cmdDeleteCommand = New RelayCommand(AddressOf DeleteExecute, AddressOf CanDeleteExecute)
End If
Return _cmdDeleteCommand
End Get
End Property
Public ReadOnly Property InsertCommand() As ICommand
Get
If _cmdInsertCommand Is Nothing Then
_cmdInsertCommand = New RelayCommand(AddressOf InsertExecute, AddressOf CanInsertExecute)
End If
Return _cmdInsertCommand
End Get
End Property
Public ReadOnly Property NextCommand() As ICommand
Get
If _cmdNextCommand Is Nothing Then
_cmdNextCommand = New RelayCommand(AddressOf NextExecute, AddressOf CanNextExecute)
End If
Return _cmdNextCommand
End Get
End Property
Public ReadOnly Property PreviousCommand() As ICommand
Get
If _cmdPreviousCommand Is Nothing Then
_cmdPreviousCommand = New RelayCommand(AddressOf PreviousExecute, AddressOf CanPreviousExecute)
End If
Return _cmdPreviousCommand
End Get
End Property
Public ReadOnly Property SaveCommand() As ICommand
Get
If _cmdSaveCommand Is Nothing Then
_cmdSaveCommand = New RelayCommand(AddressOf SaveExecute, AddressOf CanSaveExecute)
End If
Return _cmdSaveCommand
End Get
End Property
Public ReadOnly Property ViewDetailsCommand() As ICommand
Get
If _cmdViewDetails Is Nothing Then
_cmdViewDetails = New RelayCommand(AddressOf ViewDetailsExecute, AddressOf CanViewDetailsExecute)
End If
Return _cmdViewDetails
End Get
End Property
#End Region
A questo punto passiamo ai membri dell'interfaccia IDataErrorInfo. Anche in questo caso, nulla di complicato e già discusso precedentemente:
#Region " IDataErrorInfo members "
Public ReadOnly Property [Error] As String Implements System.ComponentModel.IDataErrorInfo.Error
Get
Return TryCast(Me.Order, IDataErrorInfo).Error
End Get
End Property
Default Public ReadOnly Property Item(ByVal columnName As String) As String Implements System.ComponentModel.IDataErrorInfo.Item
Get
Dim [error] As String = Nothing
[error] = (TryCast(Me.Order, IDataErrorInfo))(columnName)
' Dirty the commands registered with CommandManager,
' such as our Save command, so that they are queried
' to see if they can execute now.
CommandManager.InvalidateRequerySuggested()
Return [error]
End Get
End Property
#End Region
Le cose si fanno più interessanti perché, prima di fare altro, scriviamo il codice del costruttore.
Quando il gioco si fa duro...
Il costruttore del nostro ViewModel svolge un ruolo fondamentale. Al suo interno, infatti, dovremo registrare e istanziare le classi di servizio che eseguiranno l'effettivo lavoro di accesso ai dati tramite Entity Framework. Prima il codice, poi le considerazioni:
#Region " Constructors "
Public Sub New()
Me._customers = New ObservableCollection(Of Customer)
'Registra l'istanza della classe di servizio relativa ai clienti
ServiceLocator.RegisterService(Of ICustomerDataService)(New CustomerDataService)
'Registra l'istanza della classe di servizio relativa agli ordini
ServiceLocator.RegisterService(Of IOrderDataService)(New OrderDataService)
'Ottiene l'istanza delle due classi
Dim dataAccess = GetService(Of ICustomerDataService)()
Me.orderAccess = GetService(Of IOrderDataService)()
'Cicla l'elenco dei clienti ottenuto e lo aggiunge
'alla collezione
For Each element In dataAccess.GetAllCustomers
Me._customers.Add(element)
Next
'Imposta la CollectionViewSource
_customerViewSource.Source = Me.Customers
'Ottiene la View della CollectionViewSource e la converte
'in una ListCollectionView con supporto alla modifica
Me.CustomersView = CType(Me.CustomerViewSource.View, ListCollectionView)
Me.CustomersView.MoveCurrentToFirst()
End Sub
#End Region
Tramite il metodo condiviso RegisterService della classe ServiceLocator, registriamo due istanze per le classi di servizio CustomerDataService e OrderDataService. Notate che il parametro generico di RegisterService non è la classe, ma l'interfaccia. Ecco perché diventa importante scrivere delle interfacce che poi vengono implementate. Tramite il metodo GetService esposto dalla classe base, poi otteniamo l'istanza delle classi di servizio. Quella relativa agli ordini è dichiarata a livello di classe perché la useremo, a breve, all'interno dei command methods. Un primo esempio d'uso è il ciclo For Each che consente di popolare il campo relativo all'elenco clienti; notate, infatti, come si invochi il metodo GetAllCustomers esposto dalla classe di servizio. Qui la cosa è di fondamentale interesse: il ViewModel ottiene un elenco di dati ma non sa che tali dati provengono da un Entity Data Model. Il resto, poi, è cosa nota.
Proseguiamo con i command methods
Ora ci occupiamo di implementare i metodi per l'esecuzione dei commands, in linea generale i metodi CanXXXExecute e XXXExecute, dove XXX è il nome dell'azione da eseguire. Non c'è nulla di particolarmente difficile, abbiamo parlato di questo discorso qui. Però ci sono da notare alcune cose. Ogni metodo di esecuzione richiama il corrispondente metodo esposto dalla classe di servizio. Quindi, mentre in scenari diversi abbiamo visto come eseguire le azioni nei confronti dei dati direttamente all'interno di questi metodi, qui deleghiamo il tutto alla classe di servizio. In questo modo, il ViewModel è comunque in grado di lavorare con i dati ma è astratto dal Data Access Layer, che in questo caso è rappresentato da Entity Framework a cui si rivolge la classe OrderDataService. Quindi se un domani vorrò cambiare sorgente dati, pur con le ovvie attenzioni del caso, il mio ViewModel può rimanere sostanzialmente lo stesso. C'è un altro dettaglio interessante, di cui vi do conto dopo il codice:
#Region " Command Methods "
Private Function CanDeleteExecute(ByVal param As Object) As Boolean
If Me.CustomerOrdersView Is Nothing Then Return False
Return Me.CustomerOrdersView.CurrentPosition > -1
End Function
Private Sub DeleteExecute(ByVal param As Object)
Me.orderAccess.Delete(Me.CustomerOrdersView)
End Sub
Private Function CanInsertExecute(ByVal param As Object) As Boolean
Return True
End Function
Private Sub InsertExecute(ByVal param As Object)
Me.orderAccess.Insert(Me.CustomerOrdersView, Me.Selection)
End Sub
Private Function CanNextExecute(ByVal param As Object) As Boolean
If Me.CustomerOrdersViewSource.View Is Nothing Then Return False
Return Me.CustomerOrdersViewSource.View.CurrentPosition <
CType(Me.CustomerOrdersViewSource.View, CollectionView).Count - 1
End Function
Private Sub NextExecute(ByVal param As Object)
If Me.CanNextExecute(param) Then
Me.orderAccess.MoveToNext(Me.CustomerOrdersViewSource)
End If
End Sub
Private Function CanPreviousExecute(ByVal param As Object) As Boolean
If Me.CustomerOrdersViewSource.View Is Nothing Then Return False
Return Me.CustomerOrdersViewSource.View.CurrentPosition > 0
End Function
Private Sub PreviousExecute(ByVal param As Object)
If Me.CanPreviousExecute(param) Then
Me.orderAccess.MoveToPrevious(Me.CustomerOrdersViewSource)
End If
End Sub
Private Function CanSaveExecute(ByVal param As Object) As Boolean
Try
If CType(Me.CustomerOrdersView.CurrentItem, Order).HasErrors Then
Return False
Else
Return True
End If
Catch ex As Exception
End Try
End Function
Private Sub SaveExecute(ByVal param As Object)
Me.orderAccess.Save()
End Sub
Private Function CanViewDetailsExecute(ByVal param As Object) As Boolean
Return Me.Selection IsNot Nothing
End Function
Private Sub ViewDetailsExecute(ByVal param As Object)
Application.Msn.NotifyColleagues(Application.VIEW_DETAILS_EXECUTE)
End Sub
#End Region
L'ultimo metodo, ViewDetailsExecute, fa finalmente utilizzo della classe Messenger. Il metodo NotifyColleagues sta inviando il messaggio definito nella costante VIEW_DETAILS_EXECUTE di modo che, quando il messaggio viene intercettato dalla View, verrà aperta la finestra relativa agli Order Details. Per quanto riguarda il lato della View lo vedremo al termine della serie di post, nel quale faremo un raccordo con le spiegazioni inerenti questa tecnica.
Cambio di selezione
L'ultimo step di questo OrdersViewModel è la gestione di un evento. Poiché abbiamo necessità di capire quando il nostro utente seleziona un Customer differente tramite la UI, possiamo gestire l'evento CurrentChanged sulla CollectionView che espone i Customer. Quando l'evento viene intercettato, si caricano gli ordini relativi al nuovo cliente selezionato. Il tutto avviene ancora invocando i metodi dalla classe di servizio OrderDataService. Ecco il codice, che chiude anche la classe:
#Region " Event handlers"
'_customersView è il backing field per la proprietà CustomersView
Private Sub _customersView_CurrentChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles _customersView.CurrentChanged
Dim currentCustomer = CType(Me.CustomersView.CurrentItem, Customer)
Try
Me.Orders = orderAccess.GetAllOrders(currentCustomer.CustomerID)
Me.CustomerOrdersViewSource.Source = Me.Orders
Me.CustomerOrdersView = CType(Me.CustomerOrdersViewSource.View, ListCollectionView)
Catch ex As Exception
End Try
End Sub
#End Region
End Class
Passiamo ora al secondo ViewModel, quello relativo agli Order Details.
Parte due: OrderDetailsViewModel
Il ViewModel per la gestione degli Order Details funziona, come logica di base, in modo analogo al precedente anche se con minori funzionalità. Diciamo che per quanto riguarda la nostra applicazione a noi interesserà semplicemente mostrare l'elenco degli Order Details ma implementeremo anche dei metodi di salvataggio dati e chiusura, soprattutto perché quest'ultima informazione potrebbe essere utile alla View chiamante. Anche in questo ViewModel faremo uso di un'istanza della classe OrderDataService, secondo le modalità viste nel precedente ViewModel, per invocare il metodo che ci consentirà di ottenere l'elenco degli order details per l'ordine specificato. Tutte le tecniche usate in questo ViewModel sono state già discusse nel precedente, per cui se qualcosa non è chiaro potete riguardare la precedente sezione. Questo, invece, è il codice del nuovo OrderDetailsViewModel che va aggiunto, come classe, alla cartella di progetto chiamata ViewModels:
Imports System.Collections.ObjectModel
Public Class OrderDetailsViewModel
Inherits ViewModelBase
#Region " Declarations "
Private _cmdSaveCommand As ICommand
Private _cmdCloseCommand As ICommand
Private _objOrder_Detail As Order_Detail
Private _orderDetails As ObservableCollection(Of Order_Detail)
Dim _selection As Order_Detail
Private _orderDetailsViewSource As CollectionViewSource
Private _orderDetailsView As ListCollectionView
Private dataAccess As IOrderDataService
#End Region
#Region " Properties "
Public Property OrderDetailsViewSource As CollectionViewSource
Get
Return Me._orderDetailsViewSource
End Get
Set(ByVal value As CollectionViewSource)
Me._orderDetailsViewSource = value
OnPropertyChanged("OrderDetailsViewSource")
End Set
End Property
Public Property OrderDetailsView As ListCollectionView
Get
Return Me._orderDetailsView
End Get
Set(ByVal value As ListCollectionView)
Me._orderDetailsView = value
OnPropertyChanged("OrderDetailsView")
End Set
End Property
Public Property Order_Detail() As Order_Detail
Get
Return _objOrder_Detail
End Get
Set(ByVal Value As Order_Detail)
_objOrder_Detail = Value
OnPropertyChanged("Order_Detail")
End Set
End Property
Public Property Order_Details As ObservableCollection(Of Order_Detail)
Get
Return Me._orderDetails
End Get
Set(ByVal value As ObservableCollection(Of Order_Detail))
Me._orderDetails = value
OnPropertyChanged("Order_Details")
End Set
End Property
Public Property Selection As Order_Detail
Get
Return Me._selection
End Get
Set(ByVal value As Order_Detail)
If value Is _selection Then
Return
End If
_selection = value
MyBase.OnPropertyChanged("Selection")
End Set
End Property
#End Region
#Region " Command Properties "
Public ReadOnly Property SaveCommand() As ICommand
Get
If _cmdSaveCommand Is Nothing Then
_cmdSaveCommand = New RelayCommand(AddressOf SaveExecute, AddressOf CanSaveExecute)
End If
Return _cmdSaveCommand
End Get
End Property
Public ReadOnly Property CloseCommand As ICommand
Get
If _cmdCloseCommand Is Nothing Then
_cmdCloseCommand = New RelayCommand(AddressOf CloseExecute, AddressOf CanCloseExecute)
End If
Return _cmdCloseCommand
End Get
End Property
#End Region
#Region " Constructors "
Public Sub New(ByVal OrderID As Integer)
Me._orderDetails = New ObservableCollection(Of Order_Detail)
ServiceLocator.RegisterService(Of IOrderDataService)(New OrderDataService)
Me.dataAccess = GetService(Of IOrderDataService)()
Me._orderDetails = dataAccess.GetOrderDetailsByOrderId(OrderID)
Me._orderDetailsViewSource = New CollectionViewSource
Me.OrderDetailsViewSource.Source = Me.Order_Details
Me.OrderDetailsView = CType(Me.OrderDetailsViewSource.View, ListCollectionView)
End Sub
#End Region
#Region " Command Methods "
Private Function CanSaveExecute(ByVal param As Object) As Boolean
Return True
End Function
Private Sub SaveExecute(ByVal param As Object)
Me.dataAccess.Save()
End Sub
Private Sub CloseExecute(ByVal param As Object)
Application.Msn.NotifyColleagues(Application.VIEW_DETAILS_CLOSE)
End Sub
Private Function CanCloseExecute(ByVal param As Object) As Boolean
Return True
End Function
#End Region
End Class
Notate come, anche qui, venga richiamato il metodo Messenger.NotifyColleagues in fase di chiusura dell'oggetto. Questo può essere utile nel caso in cui dobbiamo far sapere al chiamante che la View associata si stia chiudendo. Quindi ora abbiamo tutta l'infrastruttura che lavora con i dati, passando per uno strato di servizi che permette ai ViewModel di non sapere con chi sta dialogando, ma di usarne le fonti dati. Bello, no?
Fine della parte
Questo lungo post si conclude qui, dopo un lungo lavoro. Riepilogando abbiamo:
-
collocato la dichiarazione di istanza della classe Messenger
-
implementato una classe ViewModelBase
-
implementato due ViewModel, uno per visualizzare la relazione customer/orders e uno per visualizzare gli order details
Ci manca l'ultimo passaggio, ossia definire le View. Ma non lo faremo nel prossimo post, bensì tra due. Infatti nel prossimo post faremo una digressione che ritengo interessante: faremo innanzitutto un po' di refactoring del codice, ma soprattutto scriveremo degli unit test per testare porzioni del nostro codice ed osservare uno dei benefici più importanti del pattern MVVM.
Alessandro
Volevo segnalarvi che sono disponibili, gratuitamente e "full", alcuni ulteriori estratti dal mio libro "Visual Basic 2010 Unleashed". Si tratta in realtà dei capitoli Web-only, ossia quelli che per motivi di spazio non abbiamo potuto pubblicare su carta ma che sono disponibili per tutti, in formato PDF. Eccoli, con i link:
Chapter 56: Advanced IDE Features.
Chapter 57: Introducing the Visual Studio Extensibility