Bagikan melalui


Dynamic UI with WPF and LINQ

Lately I've been getting my hands deep into WPF with my line-of-business (LOB)/data-based application mind set. I'm taking a different approach to the technology resisting the urge to put on my amateur-designer hat and instead purely focus on data, data-binding, formatting controls, and some basic layout. (Yes before you ask, I have started producing the WPF forms over data videos!)

Today I wanted to play with dynamically creating XAML and loading it at runtime. This was really easy using XML literals and XML namespace imports. Let me show you what I mean. You can load and save XAML at runtime using the System.Windows.Markup.XamlReader and System.Windows.Markup.XamlWriter classes. Create a new WPF project and in the Window1 code-behind you can do this:

 Imports <xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup

Class Window1

    Private Sub Window1_Loaded() Handles MyBase.Loaded

        Dim UI = <Label Name="Label1">This is COOL!</Label>

        Me.Content = XamlReader.Load(UI.CreateReader())

    End Sub
End Class

When we run it:

There are a lot of possibilities here. For instance, wouldn't it be nice to automatically generate all your maintenance screens in your business apps? When I say "maintenance" I mean all those simpler lookup data tables or contact tables. Even a customer contact form is usually pretty basic. Instead of handing those screens to the newbie or the junior on the team why not just generate them all at runtime based on your object model or database schema?

Admittedly this isn't something that is only unique to WPF. You could do this in Winforms as well but it was an exercise in coding the layout by hand. WPF makes this a breeze because we can define one piece of XAML and we can construct it from a single LINQ query. You can use reflection to look at your object model but if it's really simple and maps one-to-one to your database table anyway you can just create a table (or a stored proc) that contains the schema (or meta-data) of all of your maintenance tables. For instance to generate a simple UI we would probably want to obtain at minimum the following properties for each column in a table:

ColumnName
DataType
MaxLength
IsPrimaryKey

You can either create a meta-data table in your database or a stored proc that returns the info from the information schema (check permissions for the stored proc if you go that route). I was playing with the stored proc idea because then if I make a change to my database, I don't need to update my code necessarily. So for this example I created a stored proc in the Northwind database called GetTableSchema:

 CREATE PROCEDURE dbo.GetTableSchema
    (
    @table varchar(50)
    )    
AS
SELECT 
c.table_name As TableName, 
c.column_name As ColumnName, 
c.data_type As DataType, 
c.character_maximum_length As MaxLength,
    COALESCE (
    ( SELECT 
        CASE cu.column_name
            WHEN null THEN 0
            ELSE 1
        END
    FROM information_schema.constraint_column_usage cu
    INNER join information_schema.table_constraints ct
    ON ct.constraint_name = cu.constraint_name
    WHERE 
    ct.constraint_type = 'PRIMARY KEY' 
    AND ct.table_name = c.table_name
    AND cu.column_name = c.column_name 
    ),0) AS IsPrimaryKey
FROM information_schema.columns c
INNER JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE @table = t.table_name and 
      (t.table_type = 'BASE TABLE' and not 
      (t.table_name = 'dtproperties') and not 
      (t.table_name = 'sysdiagrams'))
ORDER BY c.table_name, c.ordinal_position

I then added a new "LINQ to SQL classes" item to my project and dragged Customer onto the design surface (for this example that's what we'll be editing) from the Northwind database I had connected to in my Server Explorer. I then expanded the "Stored Procedures" and dragged the above procedure onto the methods pane. Next I manually created an object on the design surface called TableSchema that contained the same properties as the fields I'm returning from the stored proc. Once I have that all set up I can now map the result type of the stored proc to the TableSchema class:

Okay now that we have that set up we can get back to the fun stuff. I created a simple lookup textbox and "Find" and "Save" buttons at a fixed area at the top of my WPF window. Under that I dragged a ContentControl onto the form and named it DynamicContent. We're going to generate the content here from the Customer schema and bind to the customer object that is returned from our LINQ query when we click find.

 <Window x:Class="Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1"  Name="Window1" SizeToContent="WidthAndHeight" >
    <Grid Name="MainGrid"  >
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="60*" />
        </Grid.RowDefinitions>
        <StackPanel Name="StackPanel1" Orientation="Horizontal" Margin="3" VerticalAlignment="Top">
            <Label Height="28" Name="Label1" Width="84" HorizontalContentAlignment="Right" FontWeight="Bold">ID</Label>
            <TextBox Height="25" Name="txtSearch" Width="120">ALFKI</TextBox>
            <Button Height="25" Name="btnFind" Width="75">Find</Button>
            <Button Height="25" Name="btnSave" Width="75">Save</Button>
        </StackPanel>
         <ContentControl Grid.Row="1" Name="DynamicContent" Margin ="3" /> 
    </Grid>
</Window>

First let's generate the UI in the Load event of our form. The first thing is to add the appropriate Imports at the top of the file and then we can generate our UI. (I also have handlers here for the actual loading and saving of the customer.)

 Imports <xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup

Class Window1

    Dim db As New NorthwindDataContext
    Dim CustomerData As Customer

    Private Sub Window1_Loaded() Handles MyBase.Loaded

        Dim customerSchema = db.GetTableSchema("Customers").ToList()

        Dim UI = <Grid xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation">
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="100*"/>
                         <ColumnDefinition Width="200*"/>
                     </Grid.ColumnDefinitions>
                     <StackPanel Name="StackLabels" Margin="3">
                         <%= From column In customerSchema _
                             Where column.IsPrimaryKey = 0 _
                             Select <Label
                                        Height="28"
                                        Name=<%= column.ColumnName & "Label" %>
                                        HorizontalContentAlignment="Right">
                                        <%= column.ColumnName %>:</Label> %>
                     </StackPanel>
                     <StackPanel Grid.Column="1" Name="StackFields" Margin="3">
                         <%= From column In customerSchema _
                             Where column.IsPrimaryKey = 0 _
                             Select GetUIElement(column) %>
                     </StackPanel>
                 </Grid>

        Me.DynamicContent.Content = XamlReader.Load(UI.CreateReader)

    End Sub

    Private Function GetUIElement(ByVal column As TableSchema) As XElement
        Select Case column.DataType
            Case "datetime", "int"
                Return <TextBox
                           Height="28"
                           Name=<%= "txt" & column.ColumnName %>
                           Text=<%= "{Binding Path=" & column.ColumnName & ", ValidatesOnDataErrors=True}" %>/>
            Case "bit"
                Return <CheckBox
                           HorizontalContentAlignment="Left"
                           Name=<%= "chk" & column.ColumnName %>
                           IsChecked=<%= "{Binding Path=" & column.ColumnName & ", ValidatesOnDataErrors=True}" %>>
                           <%= column.ColumnName %>
                       </CheckBox>
            Case Else
                Return <TextBox
                           Height="28"
                           Name=<%= "txt" & column.ColumnName %>
                           MaxLength=<%= column.MaxLength %>
                           Text=<%= "{Binding Path=" & column.ColumnName & ", ValidatesOnDataErrors=True}" %>/>
        End Select
    End Function

    Private Sub btnFind_Click() Handles btnFind.Click
        If Me.txtSearch.Text <> "" Then
            Me.CustomerData = (From cust In db.Customers _
                              Where cust.CustomerID = Me.txtSearch.Text).FirstOrDefault()

            Me.DataContext = Me.CustomerData
        Else
            Me.DataContext = Nothing
        End If
    End Sub

    Private Sub btnSave_Click() Handles btnSave.Click
        If Me.DataContext IsNot Nothing Then
            Try
                db.SubmitChanges()
                MsgBox("Saved")

            Catch ex As Exception
                MsgBox(ex.ToString)
            End Try
        End If
    End Sub
End Class

The stored proc will work for any table contained in the database, so we could even abstract this further, and I obviously didn't get very fancy with the UI -- but I think you get the idea.

UPDATE: Read the latest post on dynamic data entry and download the code samples .

Enjoy!

Comments

  • Anonymous
    June 12, 2008
    PingBack from http://blog.a-foton.ru/2008/06/13/dynamic-ui-with-wpf-and-linq/

  • Anonymous
    June 12, 2008
    I can't help but notice how much your stitching together of markup looks like a classic asp app I wrote years ago! Ahhh the memories.

  • Anonymous
    June 13, 2008
    Hi Dave, The biggest advantage here though is that all that code is compiled, not script ;-) -B

  • Anonymous
    June 13, 2008
    The comment has been removed

  • Anonymous
    June 13, 2008
    Hi Greg, Check the Name attribute of your ContentControl in the XAML file for Window1. "DynamicContent" is what I named mine in my example but by default it's just ContentControl1. The Name property on the controls in the XAML is what dictates what's exposed in the code-behind. HTH, -B

  • Anonymous
    June 13, 2008
    The comment has been removed

  • Anonymous
    June 13, 2008
    The comment has been removed

  • Anonymous
    June 13, 2008
    Thanks. I will try prefixing the WPF namespace. That is a good idea. I was going the separate code file route but it turns out at some point my xml data and the XAML xml are simply going to have to coexist.

  • Anonymous
    June 13, 2008
    Prefixing the WPF namespace worked great. I thought I would go ahead, though, and work with my xml data in a namespace. I'd like to report what I think are the implications of that:

  1. With an Imports <xmlns="http://etc...">">http://etc...">, you can use the XB literal syntax for accessing axis properties just fine: x.<child>.<grandchild> etc.
  2. If you use the .Element() syntax, you have to qualify the name: Dim ns as XNamespace = "http://etc..." x.Element(ns+"child")
  3. If your XAML binds to an XElement in this namespace, you have to fully qualify it in the binding path: ="{Binding Path=Element[{http://etc...}child]"}" Is that the shape of things or am I missing something?
  • Anonymous
    June 13, 2008
    I'm sorry, item 1) should read
  1. With an Imports <xmlns="http://etc...">, you can use the VB literal syntax for accessing axis properties just fine: x.<child>.<grandchild>
  • Anonymous
    June 13, 2008
    Hello, I have a question. Currently I'm using a lot of programming software like vb6, vba, Delphi and some more. I'd like to know what's the next programming software of Microsoft? I have seen .NET and a couple of other development software but I am not sure which one is the best. Hope you can tell me. Please mail me on the e-mail address below. Best regards, Perry vba2007@home.nl

  • Anonymous
    June 14, 2008
    Beautiful! Beautiful!  Awesomely beautiful Beth.  That's some powerful stuff.

  • Anonymous
    June 14, 2008
    Hi  Beth, Can you tell me what are the data types of the properties in the TableSchema table? Thanks, Minh

  • Anonymous
    June 14, 2008
    In order to get the prefixed XAML namespace to work with a XAML fragment containing an attached property attribute, I had to prefix the attribute name also: Imports <xmlns:p="http://schemas...presentation"> ... <p:Rectangle p:Grid.Row="0" Fill="#73B2F5"/> Otherwise XamlReader gave me this error: XML namespace prefix does not map to a namespace URI, so cannot resolve property 'Grid.Row'.

  • Anonymous
    June 16, 2008
    Hi, Is it possible to do this kind of thing and allow the dynamically to use an event handler in the code. I know this doesn't compile but it shows the kind of thing I am on about... Class Window1    Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded        Dim name = "atest"        Dim ui = <Button x:Name=<%= name %> Width="100" Height="30" Click="buttonHandler">Click Me</Button>        Me.Content = XamlReader.Load(ui.CreateReader)    End Sub    Private Sub buttonHandler(ByVal sender As Object, ByVal e As System.EventArgs)        MessageBox.Show(CType(sender, Button).Name + " was pressed")    End Sub End Class Thanks, Jason

  • Anonymous
    June 16, 2008
    Hi Minh, TableName = varchar(50) (String) ColumnName = varchar(50) (String) DataType = varchar(25) (String) MaxLength = int (Integer) IsPrimaryKey = int (Integer) HTH, -B

  • Anonymous
    June 16, 2008
    Hi Beth, I just saw this and I'm amazed! I had no idea you could do this in VB, I mean, use xaml directly in code! Please tell if there is any way to acheive this in C#. Thanks!

  • Anonymous
    June 16, 2008
    Hi Jason, You can't specify event handlers in the dynamic XAML because it must be compiled. However you can hook up the event in code. Here's a blog post from Calvin on the team that shows you how. http://blogs.msdn.com/calvin_hsia/archive/2007/11/28/6583391.aspx HTH, -B

  • Anonymous
    June 16, 2008
    Thanks, That was exactly what I was looking for. Jas

  • Anonymous
    June 16, 2008
    Hi Bruno, XML Literals are only supported in VB. However you can use the LINQ to XML api directly to manually construct the XAML it's just not as easy. -B

  • Anonymous
    June 16, 2008
    How do you translate the following into c#? Imports <xmlns="http://schemas.microsoft.com/winfx/2006/        Dim UI = <Label Name="Label1">This is COOL!</Label> Thanks, Doug

  • Anonymous
    June 18, 2008
    The comment has been removed

  • Anonymous
    June 18, 2008
    The comment has been removed

  • Anonymous
    June 18, 2008
    Hi Greg/Gonzalo, When you create the TableSchema class you need to make sure each of the properties you are mapping are the correct type. The MaxLength and IsPrimaryKey are both Integers. HTH, -B

  • Anonymous
    June 18, 2008
    Hi Beth, Thx again for the help.  I did correct the properties for the table but that still did not fix it.  I then had to delete both the table and stored produced reference in the Northwind.dbml and re-add them, as the steps document.  After doing so, it worked.  Thx Beth for the work and help, this great! G

  • Anonymous
    June 18, 2008
    ¡Perfecto! Magnifico. Thanks Beth. I was about to say that after making sure the properties were correct it worked! Mucho más for favor? Keep WPFing with Linq please ;) Bravo G

  • Anonymous
    June 18, 2008
    MSBuild MSBuild Reserved Properties [Via: Sayed Ibrahim Hashimi ] Sharepoint Adding Copy and Paste...

  • Anonymous
    June 20, 2008
    Hi Beth, I did some of this, but  I had made my own table, because I don't have northwind loaded anywhere.  The beginning stuff worked fine, but when you got in to the depths of the code, there were fields that appeared to be dependent on that particualar table or database. Would like to work some more on it over the weekend, if you would be so kind as to provide a link to where I can download the version of it that you have, it would be appreciated. Thanks, John aka Radiolistener

  • Anonymous
    June 24, 2008
    In my last post on this subject I explored creating WPF UI's dynamically using XML literals. The one

  • Anonymous
    June 24, 2008
    Hi John (Radiolistener), I just uploaded the sample onto Code Gallery: http://code.msdn.microsoft.com/dynamicWPF Enjoy, -B

  • Anonymous
    June 25, 2008
    Thanks Beth, I'll study it over the weekend.   Working on another video.  I really want to tip my hat to you, you make creating those videos look easy, but it really is a LOT of work. Thanks, John.

  • Anonymous
    August 25, 2008
    Beth, In your example on building ui from schema table I have copied as is and get this error when calling the XamlReader.Load XML namespace prefix does not map to a namespace URI, so cannot resolve Height. As is mind you. i am doing this in c# if that is any help. Please help Malcolm

  • Anonymous
    August 26, 2008
    Hi Malcom, I believe you'll need to control the XML namespaces manually in C#. VB does this automatically with the XML namespace imports at the top of the file. Take a look at: http://msdn.microsoft.com/en-us/library/bb387069.aspx HTH, -B

  • Anonymous
    October 08, 2008
    In case anyone else runs into this, I had to change information_schema to upper case in the stored proc because my db was set to case sensitive.

  • Anonymous
    October 09, 2008
    Excellent post! Any thoughts on converting this to a web app?

  • Anonymous
    October 29, 2008
    Hi Beth Excellent Article.What to I have to do if I want to try it out the same in C#

  • Anonymous
    December 01, 2008
    Hi Beth, Excellent post, this helps me to develop a dynamioc WPF appilication in VB.Net I am trying to do the same in C#, could you please help me in this regard? Thanks in Advance Krishna Prasad rvkrishnaprasad@gmail.com

  • Anonymous
    December 01, 2008
    hi Beth, your example on building ui from schema table is very good. I have tried to develop the same in c#, but unfortunately i didn't. Please help me by providing a sample c# code to build wpf ui from schema. your help is highly appreciated. Thanks. Lakshmi Narayana Vejendla narayana.vejendla@hotmail.com

  • Anonymous
    December 01, 2008
    Hi Beth, Excellent post, this helps me to develop a dynamic WPF appilication in VB.Net I am trying to do the same in C#, could you please help me in this regard? Thanks in Advance Krishna Prasad rvskrishnaprasad@gmail.com krishnaprasad_rvs@rediffmail.com

  • Anonymous
    February 27, 2009
    Thanks Beth for the excellent article. I've included a link below to a C# port of your primary example (just the .cs file and only the dynamic display bits, not the find/save methods). I'm teaching an internal class on WPF and XAML and wanted an example of how XAML can be used dynamically. However, since we only develop in C#, I needed to port it...I didn't realize how differently VB handles XML until I started the port. The first attempt just used StringBuilder to manually build the XML. Once I figured out the syntactic differences, I had no problems what-so-ever. However, I then decided to utilize the new XDocument/XElement classes. I immediately ran into the namespace issues mentioned by others. After dozens of Google searches and numerous tests, trials and tribulations, I found that when you have a default namespace, you simply need to prepend all elements with the namespace, and you're set. A second, still unresolved issue occurred when I attempted to load the XML with the XDocument.CreateReader() method. I ran into a namespace error on the Grid.Column attribute. The work-around was to first load it into a text reader. A bit of searching didn't turn up much (or rather turned up way to much that wasn't on target), so I'll leave this for a rainy day, or someone else with the interest in debugging it. Note that I didn't attempt to make a real-world application out of this...the sole purpose is as a training exercise, so it was left in the code-behind with a hard-coded call to a single table. You'll need to follow the article to create the initial page, stored procedure and connecting bits (and add the find/save methods)...as mentioned at the top, I've only included the C# for dynamic display of the XAML. I hadn't used LINQ to SQL yet (I use one of the commercial O/R mapper's that support LINQ for production code), and Beth's step-by-step example worked perfectly the first time. MSDN Code Gallery location: http://code.msdn.microsoft.com/dynamicWpfCSharp

  • Anonymous
    March 12, 2009
    Hi Beth, I finally got VS2008 :) about time eh lol Unfortunately, I wasn't able to get the whole suite, just the express editions, which is OK for now seeing as it gives me opportunity to experiment. I was just going through this article and I noticed that, once I'd added a connection to the Northwind database that came with Visual Basic 2008, my Database Explorer didn't show me a Stored Procedures item, so I just wanted to ask if you could tell me where I can write my procs. Thanks a lot! -L

  • Anonymous
    June 16, 2009
    How to call a custom control using  XamlReader.Load, Any Idea?