Writing controls in WPF/E
A while back, I promised to share some tips and tricks about writing controls in the WPF/E CTP (yeah, I apologize, it's been a busy year). A lot of the SDK samples make use of a little framework that Peter Blois wrote which uses JavaScript's eval to create a system for hooking up delegates/event callbacks in code. That's a perfectly good way of doing things, but I prefer a little more of a xaml-oriented approach, so I wrote my own little framework to allow controls to be defined in xaml. Let's walk through an example of a simple button control. The xaml for the button looks like:
<!-- Button.xaml -->
<Canvas
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Name="button"
MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
MouseEnter="javascript:button_MouseEnter"
MouseLeave="javascript:button_MouseLeave"
>
<Canvas.RenderTransform>
<TransformGroup>
<TranslateTransform X="0" Y="0" x:Name="transform"/>
</TransformGroup>
</Canvas.RenderTransform>
<Rectangle Stroke="#FF000000" Fill="sc#1, 0.8123474, 0.8123474, 0.8123474"
Width="128.8" Height="56" x:Name="rectangle"/>
<Rectangle Stroke="sc#1, 0.912730157, 0.37122494, 0.17111966" StrokeThickness="5"
Width="126.8" Height="54" Canvas.Left="1" Canvas.Top="1"
Opacity="0"
x:Name="highlight"/>
<Glyphs Fill="black" FontRenderingEmSize="20" FontUri="segmcsb.ttf"
OriginY="35" OriginX="25" UnicodeString="Button!" >
</Glyphs>
</Canvas>
Which we could then use in our larger application xaml like this:
<Canvas ...>
<Canvas Loaded="javascript:MakeButton" Canvas.Top="100" Canvas.Left="20"/>
<Canvas Loaded="javascript:MakeButton" x:Name="button2"
Canvas.Top="100" Canvas.Left="320"
/>
</Canvas>
So the button is really a canvas element, which by calling the framework function MakeButton() takes on the look and behavior of a button control. MakeButton() is defined as:
function MakeButton(canvas) {
var buttonState = MakeControl(canvas, "Button.xaml");
buttonState.mouseOver = false;
buttonState.pressed = false;
buttonState.click = null;
}
(The "canvas" parameter is the Loaded event's sender parameter) The heavy lifting is in the MakeControl call, which loads Button.xaml. Note how in button.xaml, we specified some event handlers,
<!-- Button.xaml -->
<Canvas
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Name="button"
MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
MouseEnter="javascript:button_MouseEnter"
MouseLeave="javascript:button_MouseLeave"
>
The corresponding JavaScript (we'll just do one of those methods) is:
function button_MouseEnter(sender,args)
{
GetState(sender).mouseOver = true;
... update element tree to look different ...
}
Every button needs a little bit of data about whether it's currently being pressed, whether the mouse is over, and what methods you call when it gets clicked. This is per-button data, we can store this and globals. Now WPF/E doesn't support expandos, so we can't just write sender.mouseOver = true (although that's certainly on my wish list of features to add). So we do the next best thing and associate the sender element (the canvas) with an object we can set properties on -- and that's what the framework call GetState(sender) does.
GetState() is just one of the things that MakeControl sets up for us. MakeControl also downloads Button.xaml for us (using the standard JavaScript XmlHttpRequest -- at the time I wrote it, the WPF/E "downloader" API didn't exist). And it gives us named properties for any of the Name'd elements in button.xaml. E.g.:
<!-- Button.xaml -->
<Canvas ...>
<Rectangle ... x:Name="rectangle" />
We can access this by writing GetState(sender).rectangle.
But wait -- you can have as many buttons in your application as you want, and each one of those buttons has a rectangle with Name="rectangle"? Do you get name conflicts? That's the last major duty of MakeControl -- it actually changes your xaml each time it loads a control, to add a unique suffix to each Name'd element -- e.g., rectangle_43. Which makes GetState(sender).rectangle all the more important, since you don't really want to keep track of whether it's button #43 or button #71.
So it's a nifty little system. Of course, it does have some limitations. The biggest is that it's really hard to do control composition -- i.e., your listbox control contains a scrollbar control contains a button control. All the work happens inside the Loaded event, and the problem is that WPF/E waits for one Loaded handler to completely finish executing before it fires the next Loaded event. So MakeListBox will finish before MakeScrollBar is even called, which makes it very difficult to initialize any of the control-specific APIs like button's click event. (In this respect, Peter's framework is better)
The other big limitation is that button.xaml has to be served over http, and not over a \\unc path. After all, it's XmlHttpRequest, not XmlUncRequest. I haven't found that to be much of a problem with production code, but it always annoys me during development.
And finally, for your viewing pleasure, here's the complete source for the framework along with a sample button control:
///////////////////////////////////////////////////////
// Button
///////////////////////////////////////////////////////
/*
Button APIs:
state.pressed -- bool
state.mouseOver -- bool
state.click -- event handler
*/
// turn a Canvas into a button (by inserting Button.xaml inside the canvas)
function MakeButton(canvas) {
var buttonState = MakeControl(canvas, "Button.xaml");
buttonState.mouseOver = false;
buttonState.pressed = false;
buttonState.click = null;
}
function button_MouseLeftButtonDown(sender,args)
{
sender.CaptureMouse();
GetState(sender).mouseOver = true;
GetState(sender).pressed = true;
UpdateVisuals(sender);
}
function button_MouseLeftButtonUp(sender,args)
{
sender.ReleaseMouseCapture();
GetState(sender).pressed = false;
UpdateVisuals(sender);
if (GetState(sender).mouseOver && GetState(sender).click) {
GetState(sender).click(sender,args);
}
}
function button_MouseEnter(sender,args)
{
GetState(sender).mouseOver = true;
UpdateVisuals(sender);
}
function button_MouseLeave(sender,args)
{
GetState(sender).mouseOver = false;
UpdateVisuals(sender);
}
function UpdateVisuals(button) {
//background
var state = GetState(button);
if (state.pressed && state.mouseOver) {
state.rectangle.Fill = "sc#1, 0.548430264, 0.5354195, 0.5354195";
state.transform.X = 2;
state.transform.Y = 2;
} else {
state.rectangle.Fill = "sc#1, 0.8123474, 0.8123474, 0.8123474";
state.transform.X = 0;
state.transform.Y = 0;
}
// highlight
if (state.mouseOver || state.pressed) {
state.highlight.Opacity = 1;
} else {
state.highlight.Opacity = 0;
}
}
<!-- Button.xaml -->
<Canvas
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Name="button"
MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
MouseEnter="javascript:button_MouseEnter"
MouseLeave="javascript:button_MouseLeave"
>
<Canvas.RenderTransform>
<TransformGroup>
<TranslateTransform X="0" Y="0" x:Name="transform"/>
</TransformGroup>
</Canvas.RenderTransform>
<Rectangle Stroke="#FF000000" Fill="sc#1, 0.8123474, 0.8123474, 0.8123474"
Width="128.8" Height="56" x:Name="rectangle"/>
<Rectangle Stroke="sc#1, 0.912730157, 0.37122494, 0.17111966" StrokeThickness="5"
Width="126.8" Height="54" Canvas.Left="1" Canvas.Top="1"
Opacity="0"
x:Name="highlight"/>
<Glyphs Fill="black" FontRenderingEmSize="20" FontUri="segmcsb.ttf"
OriginY="35" OriginX="25" UnicodeString="Button!" >
</Glyphs>
</Canvas>
// common infrastructure for controls
///////////////////////////////////////////////////////////////
// xaml downloading logic
// smooth over various browser inconsistencies
function createXMLHttpRequest(){
var httprequest = false;
if (window.XMLHttpRequest) { // if Mozilla, Safari etc
httprequest = new XMLHttpRequest();
if (httprequest.overrideMimeType)
httprequest.overrideMimeType("text/xml");
}
else if (window.ActiveXObject){ // if IE
try {
httprequest=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e){
try{
httprequest=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e){}
}
}
return httprequest;
}
function downloadText(url) {
var request = createXMLHttpRequest();
request.open("GET", url, false);
request.send(null);
if (request.status == 200) {
var text = request.responseText;
return text;
}
else {
alert("Error loading "+url);
}
}
// synchronously loads the xaml and returns the root DependencyObject
function downloadXaml(url) {
var text = downloadText(url);
var element = wpfeControl.createFromXaml(text);
return element;
}
// the callback is called if/when the download is completed successfully,
// and receives the parsed DependencyObject as its parameter
function downloadXamlAsync(url, callback) {
var request = createXMLHttpRequest();
request.onreadystatechange = function (evt) {
if (request.readyState == 4) {
if (request.status == 200) {
var text = request.responseText;
var element = wpfeControl.createFromXaml(text);
callback(element);
}
else {
alert("Error loading "+url);
}
}
};
request.open("GET", url);
request.send();
}
///////////////////////////////////////////////////////////////
// control initialization logic
// usage: partsToStates[interestingobject.name] = new Object();
// then partsToStates[interestingobject.name].property = foo;
var partsToStates = new Object();
// takes an element inside a template, and returns the
// corresponding control's state object.
function GetState(element) {
var name = element.Name;
if (name == "") {
throw "element must be named to have a state object";
}
return partsToStates[name];
}
var containersToStates = new Object();
// Given a container (canvas), gets the state object for the
// control this container contains
function GetControlForContainer(canvas) {
var name = canvas.Name;
if (name == "") {
throw "element must be named to have a state object";
}
return containersToStates[name];
}
// used to generate unique IDs
var controlCounter = 0;
// Return value is xaml with names changed.
// Second parameter is an output parameter, list of names found
function RenameElements(xaml, namesFound) {
var result = "";
while (true) {
var regexp = /((\s|x:)((Storyboard.)?Target)?Name=('|"))(\w+)('|")/;
var matches = regexp.exec(xaml);
if (matches == null) {
result += xaml;
break;
}
var name = matches[6];
var newname = name + "_" + controlCounter.toString();
var isTargetName = (matches[4] != "");
if (!isTargetName) {
namesFound[name] = newname;
}
result += xaml.substring(0, matches.index)
+ matches[1] + newname + matches[7];
// matches.lastIndex doesn't exist on Firefox
xaml = xaml.substring(matches.index + matches[0].length);
}
return result;
}
// to do:
// parameters in markup
// check that nested controls works
// animations inside controls
// more robust parser for the xaml renaming
// returns the control's state object
function MakeControl(canvas, xamlurl) {
var xaml = downloadText(xamlurl);
var names = new Object();
xaml = RenameElements(xaml, names);
var controlID = controlCounter;
controlCounter++;
var state = new Object();
state.createFromXaml = function(text) {
return wpfeControl.createFromXaml(text);
}
containersToStates[canvas.Name] = state;
state.container = canvas;
state.templateRoot = wpfeControl.createFromXaml(xaml);
canvas.Children.Add(state.templateRoot);
for (var name in names) {
var fullname = names [name];
var element = canvas.FindName(fullname);
var sanityCheck = element.Name;
if (fullname != sanityCheck) {
throw "for some reason, the names don't match";
}
state[name] = element;
partsToStates[fullname] = state;
}
return state;
}
/*
CommonControls.js
This file demonstrates the use of some common controls
*/
function root_Loaded(sender) {
// root_Loaded will run before its children's Loaded events.
// But the initialization we want to do requires those children's
// Loaded events to run first. Therefore, we put a dummy element
// at the end of the tree, so we can get a Loaded event after
// everything else has been loaded.
}
function delayedInitialization(sender) {
// Intentionally hook up only the left button
var button2 = sender.FindName("button2");
GetControlForContainer(button2).click = function(sender) { alert("hello world"); };
}
function ErrorHandler(line, col, hr, string)
{
var str = "("+line+","+col+"): "+string+"\n";
str += "HRESULT: "+hr;
alert(str);
}
<!-- CommonControls.xaml -->
<Canvas Width="700" Height="700"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Name="root" Loaded="javascript:root_Loaded"
>
<Canvas Loaded="javascript:MakeButton" x:Name="b1" Canvas.Top="100" Canvas.Left="20"/>
<Canvas Loaded="javascript:MakeButton" x:Name="button2"
Canvas.Top="100" Canvas.Left="320"
/>
<!-- hack to get a Loaded event after all the controls have been initialized -->
<Canvas Loaded="javascript:delayedInitialization" Opacity="0"/>
</Canvas>
Comments
Anonymous
February 26, 2007
Is there any way to look how it works this example, because I’m trying run and I have a problems. In this rows I have unknown exeptions state.templateRoot = wpfeControl.createFromXaml(xaml); canvas.Children.Add(state.templateRoot); Thanks NickAnonymous
February 26, 2007
Nick has written an in-depth post on a method to write reusable widgets in WPF/E. This is really a mini-frameworkAnonymous
February 26, 2007
I fixed problems
- You must replace this code with february CTP <!-- Button.xaml --> <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Correct <Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- there wasn't font "segmcsb.ttf", after that I replace with Verdana, mini-framework run Thanks again Nick
Anonymous
March 26, 2007
The recent Proof of Concept work we've been working on has mainly involved the use of WPF/E as we lookAnonymous
March 28, 2007
Can I donwload the source code somewhere??? That would be nice. greetings me