Sharing the goodness...
Beth Massi is a Senior Program Manager on the Microsoft Visual Studio BizApps team who build the Visual Studio tools for Azure, Office, SharePoint as well as Visual Studio LightSwitch. Learn more about Beth.
More videos »
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> Imports <xmlns:x="http://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:
ColumnNameDataTypeMaxLengthIsPrimaryKey
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> Imports <xmlns:x="http://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="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://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!
PingBack from http://blog.a-foton.ru/2008/06/13/dynamic-ui-with-wpf-and-linq/
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.
Hi Dave,
The biggest advantage here though is that all that code is compiled, not script ;-)
-B
Hey Beth,
Trying out your code a got an error and dont know what to do. The line "Me.DynamicContent.Content = XamlReader.Load(UI.CreateReader)" in Sub Window1_Loaded(). The IDE doesn't recognize Me.DynamicContent. Do you know what may cause this.
Thx
G
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,
If you are working with separate xml data to manipulate or populate your XAML xml, you are in for namespace hell.
The imports statements contaminate any linq axis access to your separate xml data if it's in the default namespace.
I'm trying to work around it by adding xmlns attributes to the XAML xml, but can't figure out how to add an attribute to an XElement. It doesn't like trying to SetAttributeValue("xmlns:x","...") because of the colon in the attribute name.
Hi Charles,
There's a couple things you can do to work around this. You can place the XAML processing code into a separate class/file and just call that. You can place your other XML data into a namepace. Or you can set an explicit namespace on the XAML. For instance this will load just fine as well. Notice I'm using "c" as the xml namespace now instead of the default namespace.
Imports <xmlns:c="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup
.
Dim UI = <c:TextBox
Height="28"
Name="txtCompanyName"
MaxLength="20"
Text="{c:Binding Path=CompanyName, ValidatesOnDataErrors=True}"/>
Me.DynamicContent.Content = XamlReader.Load(UI.CreateReader())
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.
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?
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>
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
Beautiful! Beautiful! Awesomely beautiful Beth. That's some powerful stuff.
Hi Beth,
Can you tell me what are the data types of the properties in the TableSchema table?
Thanks,
Minh
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'.
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 Class
Jason