|
Dynamic WPF User Interfaces
EasyScript is a .NET scripting
tool which you can use in your
.NET programs for any scripting
need. But you can also use it
much like T4, PHP or Classic ASP
to generate portions of your WPF
or Silverlight pages dynamically
at runtime. Your page becomes a
template in which XAML and
script code are mixed and run by
EasyScript to generate your
final page.
The main use of this method is
for when you have a situation
where you need to generate an
unknown number of controls, for
example for a query result of
some sort. We encounter this
situation all the time with
websites anytime a query returns
multiple results (e.g. a Google
or eBay search), and its no
different with everyday business
programming. You can pass in
.NET objects as parameters to
the scripting context and use
them directly in the script
template as needed to generate
your page. For example, you can
pass in a DataTable parameter to
use to generate a number of
Hyperlink controls on the page
using a loop. You run the script
when its time to show the Page
or Window, assigning the output
of the script to the Content
property of the Window. The cool
thing is, you still use the
Visual Studio or Blend page
editor to create and maintain
your pages. The key is you embed
the script using XML comments
(<!-- -->) so it doesn't
interfere with the the XAML page
editor.
Of course, you can dynamically
add controls to the XAML page in
code, but this is messy.
Server-side webpage scripting
tools solve this problem by
mixing code with the text of the
page. Each page is
essentially a standalone program
which is meant to generate an
HTML page. This is the
same concept we use with
EasyScript to generate a XAML
page.
If you have already read the
Overview page, you know how
EasyScript works. We will
now show how to use EasyScript
to generate XAML pages
dynamically.
An
Example
Let's use the sample download
project for our example.
In this scenario, we have a form
where the user can enter the
first characters of a company
name to display all customers
which match the criteria.
Each customer is represented by
a checkbox for the user to tick.
When the Submit button is
clicked the customers with
ticked checkboxes are discovered
for further processing.
Here is a screenshot showing all
customers whose names begin with
letter A.

If there are too many checkboxes
to fit on in the space, a
scrollbar will appear on the
page, just like on a webpage.
The checkboxes were generated
dynamically with the script
shown here, which we will
explain below.
<ScrollViewer
Margin="0,0" Name="scrollViewer1"
VerticalScrollBarVisibility="Auto" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Grid> <StackPanel Name="stackPanel1"
Margin="0,0">
<!-- ds.MoveFirst(); cols = ds.Columns; n = 0;
while( !ds.eof ) { n++;
compName = cols.GetColumn_("CompanyName").Value;
compName = compName.Replace( "&", "&" );
Host.Write( "<CheckBox Height='16' Name='cb" +
n.ToString() + "' Width='auto'>" );
Host.Write( compName );
Host.WriteLn( "</CheckBox>" );
ds.MoveNext(); } --> </StackPanel> <Button Height="32" Name="BtnClose"
Width="62"
HorizontalAlignment="Right"
Margin="0,0,6,6"
VerticalAlignment="Bottom">Close</Button> <Label Height="26" Margin="0,0"
Name="label1"
VerticalAlignment="Top">Enter
search string (e.g. A)</Label> <TextBox Height="23" Margin="6,22,133,0"
Name="TxtSearch"
VerticalAlignment="Top"
/> <Button Height="32"
HorizontalAlignment="Right"
Margin="0,0,6,62"
Name="BtnSubmit"
VerticalAlignment="Bottom"
Width="62">Submit</Button> <Button Height="23"
HorizontalAlignment="Right"
Margin="0,22,48,0"
Name="BtnSearch"
VerticalAlignment="Top"
Width="75">Search</Button> </Grid> </ScrollViewer> |
OK, so how does it all work?
The first thing to understand is
that we still use the Visual
Studio XAML page editor to
create and maintain your XAML
pages. The key is that you
enclose your script code in XML
comments (<!-- -->) as shown
below.

So the first thing you do is create
your XAML page. Then in the place where you need
to generate your UI elements you insert your script code
in comments. Having the code enclosed in comments
is required so that you don't get XAML compile errors.
Let's examine the script code which creates the
checkboxes.
<!-- ds.MoveFirst(); cols = ds.Columns; n = 0;
while( !ds.eof ) { n++;
compName = cols.GetColumn_("CompanyName").Value;
compName = compName.Replace( "&", "&" );
Host.Write( "<CheckBox Height='16' Name='cb" +
n.ToString() + "' Width='auto'>" );
Host.Write( compName );
Host.WriteLn( "</CheckBox>" );
ds.MoveNext(); } --> |
We have passed the variable ds into the the
script engine context. It is a SqlitePlus Dataset
(using our SqlitePlus COM DLL) which contains all of the
customers returned from the query. The code loops
through the dataset, writing a CheckBox declaration for
each row in the Dataset.
OK, but how do we run the script? Obviously WPF
will just ignore the comments in the XAML. So what
we do is copy everything in the XAML code except the
Window declaration. We will store this code as
a text file resource in the project to be loaded and run
when its time to show the window, as shown here:

I named the script files as .xaml.script, but it
doesn't matter what you name it. You are going to
read this file in your code and feed it to EasyScript.
Then we will set it into the Content property of the
Window. Here are the steps to do that:
- Execute the query which we will use to populate
the page.
- Read the script text from the embedded resource.
- Instantiate an EasyScript.Scripter object and
execute the script to generate the XAML.
- Use the XamlReader to load the XAML and assign
it to the Content property.
- Assign control variables and wire up any events.
Here's the code.
// In Form1.xaml.cs
public void Load()
{
SqlitePlus.SqliteDb db = null;
try
{
string searchText = string.Empty;
if( Content != null )
{
TxtSearch = (TextBox)
LogicalTreeHelper.FindLogicalNode( (DependencyObject)
Content, "TxtSearch"
);
// get the search text, if
any
if( TxtSearch != null )
searchText = TxtSearch.Text.Trim();
}
// get the requested customers
db = MainWin.GetOpenDb();
string sql = string.Format( "SELECT *
FROM Customers WHERE CompanyName LIKE
'{0}%'", searchText );
SqlitePlus.ErrorCodeEnum ec;
string sErr;
SqlitePlus.Dataset ds = db.Execute( sql, null, out ec, out
sErr );
// add the Dataset to the script
context
List<Parameter> parameters = new List<Parameter>();
parameters.Add( new Parameter( "ds",
ds ) );
// Load the script from the resource
StreamResourceInfo info = Application.GetResourceStream(
new Uri(
"Scripts/Form1.xaml.script",
UriKind.Relative ) );
TextReader reader = new StreamReader( info.Stream );
string script = reader.ReadToEnd();
// get an EasyScript Scripter object
Scripter scriptObj = new EasyScript.Scripter();
// tell it to use start/end comment for
its code delimiters
scriptObj.StartScriptDelims = "<!--";
scriptObj.EndScriptDelims = "-->";
// Execute the script
scriptObj.RunScript( script, parameters );
// get the script output
MemoryStream strm = new MemoryStream();
StreamWriter writer = new StreamWriter( strm );
writer.Write( scriptObj.Output );
writer.Flush();
strm.Seek( 0, SeekOrigin.Begin );
// Load the XAML and assign to the
Content
DependencyObject rootElement = (DependencyObject)
XamlReader.Load( strm );
this.Content = rootElement;
// re-assign the controls and wire up
the event handlers
TxtSearch = (TextBox) LogicalTreeHelper.FindLogicalNode( (DependencyObject)
Content, "TxtSearch"
);
if( TxtSearch != null )
TxtSearch.Text = searchText;
BtnClose = (Button) LogicalTreeHelper.FindLogicalNode(
rootElement, "BtnClose"
);
if( BtnClose != null )
BtnClose.Click += BtnClose_Click;
BtnSubmit = (Button) LogicalTreeHelper.FindLogicalNode(
rootElement, "BtnSubmit"
);
if( BtnSubmit != null )
BtnSubmit.Click += BtnSubmit_Click;
BtnSearch = (Button) LogicalTreeHelper.FindLogicalNode(
rootElement, "BtnSearch"
);
if( BtnSearch != null )
BtnSearch.Click += BtnSearch_Click;
}
finally
{
if( db != null )
db.Close();
}
} |
Notice we have put all of this code in the Load
method. We call this method each time the Search
button is clicked. Each time it is run, we are
generating new content based on the search criteria and
assigning it to be the Content of the Window. Very
simple.
Some Notes
In this system, we have the code in two places, the
XAML window and the resource script file. This is
not ideal, but its not that bad either. Of course
the master code is the XAML file, which you would keep
in your source code control system. You only need
to copy it to the script file when you make a change to
the code.
Of course, you can also keep your scripts as separate
(loose) files and use Scripter.RunScriptFile. Or
you could retrieve your scripts from a database, a WCF
service or a webserver.
This example replaces the whole window's content, but
you could also use the same technique with UserControls
as well - a very powerful tool. The sample project
in the download demonstrates this technique as well.

|

|