Dynamic XAML: Ευκολότερα δεν γίνεται!


Το Windows Presentation Foundation ως markup μηχανισμός μας δίνει νέες δυνατότητες στη δημιουργία rich client εφαρμογών με στοιχεία που μέχρι σήμερα είχαμε διαθέσιμα μόνο στις ASP.NET εφαρμογές. Ένα κλασικό θέμα είναι η δημιουργία του UI on-the-fly, κατά το runtime, βάσει meta-data που μπορεί να έρχονται από οποιαδήποτε πηγή. Ήδη μου έλεγε ο Παναγιώτης Καναβός τις προάλλες για το ASP.NET Dynamic Data Support που επιτρέπει τη δημιουργία data-driven web εφαρμογών μέσω templates χωρίς να χρειάζεται να γίνει βάλει χέρι ο προγραμματιστής. Κατόπιν, σκέφτηκα κάλλιστα αυτά τα templates θα μπορούσαν να είναι σε XAML οπότε τελικά να καταλήγει κανείς σε WPF αντί ASP.NET εφαρμογή. Μάλιστα, παίζοντας τις προάλλες με το Linq To XML και τα XML Literals της VB, είπα να σκαρώσω κάτι ανάλογο. Έχουμε και λέμε λοιπόν:

Ξεκινάμε με ένα XAML window που έχει ένα button και ένα frame:

<Window x:Class="Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <Button Height="23" Name="Button1" HorizontalAlignment="Left" 
Margin="10,10,0,0" VerticalAlignment="Top" Width="75">
Button</Button> <Frame Margin="10,50,12,12" Name="Frame1" /> </Grid> </Window>

Κατόπιν, ορίζουμε μία κλάση που θα περιέχει τα meta-data

    Public Class Field
        Private _Name As String
        Public Property Name() As String
            Get
                Return _Name
            End Get
            Set(ByVal value As String)
                _Name = value
            End Set
        End Property


        Private _Description As String
        Public Property Description() As String
            Get
                Return _Description
            End Get
            Set(ByVal value As String)
                _Description = value
            End Set
        End Property
    End Class

Στη συνέχεια, στο click event του button κάνουμε τα εξής: Γεμίζουμε ένα List(Of T) με τα data. Στη συγκεκριμένη περίπτωση με χαζό τρόπο αφού τώρα δεν είναι αυτό το ζητούμενο:

Dim Fields As New List(Of Field)(New Field() _
       { _
          New Field With {.Name = "LastName", _
                          .Description = "Last Name"}, _
          New Field With {.Name = "FirstName", _
                          .Description = "First Name"}, _
          New Field With {.Name = "Salary", _
                          .Description = "Salary"} _
       })

 

Και τώρα έρχεται το μαγικό! Στο click event του Button1 αρχικά θα φτιάξουμε το template μας:

Dim GeneratedXAML As XElement = _
 <StackPanel Margin="0,46,0,0" Name="StackPanel2">
     <%= From Field In Fields Select _
         <DockPanel>
             <Label Width="100"><%= Field.Description %></Label>
             <TextBox Name=<%= Field.Name & "TextBox" %>></TextBox>
         </DockPanel> %>
 </StackPanel>

Ουσιαστικά, θα τρέξει το Linq to XML query και θα παραχθεί το παρακάτω:

<StackPanel Margin="0,46,0,0" Name="StackPanel2">
 <DockPanel>
     <Label Width="100">Last Name</Label>
     <TextBox Name="LastName"></TextBox>
 </DockPanel>
 <DockPanel>
     <Label Width="100">First Name</Label>
     <TextBox Name="FirstName"></TextBox>
 </DockPanel>
 <DockPanel>
     <Label Width="100">Salary</Label>
     <TextBox Name="Salary"></TextBox>
 </DockPanel>
</StackPanel>

Οπότε μένει να δούμε πως θα το περάσουμε κατά το runtime μέσα στο Frame. Θα χρησιμοποιήσουμε το property Content στο οποίο θα κάνουμε assign ένα νέο StackPanel που θα προέλθει από το XElement. Εδώ θα μας βοηθήσει η κλάση XamlReader η οποία επιτρέπει τον μετασχηματισμό XAML markup σε object. Ας τα δούμε όλα μαζί:

Dim parser As New ParserContext()
parser.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation")
parser.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml")

Dim stream As MemoryStream = New MemoryStream(System.Text.Encoding.UTF8.GetBytes(GeneratedXAML.ToString))
Dim newPanel As StackPanel = CType(XamlReader.Load(stream, parser), StackPanel)
Frame1.Content = newPanel

Ουσιαστικά, όλη η δουλειά γίνεται με τις δύο τελευταίες γραμμές κώδικα. Όλα τα προηγούμενα μπορούμε να τα παραλείψουμε και να μην χρησιμοποιήσουμε το MemoryStrean καθώς και το ParserContext, απλά γράφοντας

Dim newPanel As StackPanel = CType(XamlReader.Load(GeneratedXAML.CreateReader), StackPanel) 
Frame1.Content = newPanel

όμως τότε θα πρέπει να βάλουμε τα namespaces μέσα στο XML literal σε διάφορα σημεία, πράγμα που κάνει τον κώδικα λιγότερο ευανάγνωστο.

Πατάμε το κουμπάκι λοιπόν και από

Before

έχουμε

after

 

Happy Coding!