(Impressive, isn't it???)
Introduction
This is my first real foray into WPF, as I have a small (personal) project that I'd like to implement in WPF as a "getting my feet wet" exercise.
This work is heavily based on CPian "sukram" work on a WPF Diagram Designer (see References). The reason though that I'm actually writing an article about this is that, in trying to follow his work (and everyone else's, for that matter), I still found myself floundering with WPF. I have come to realize that WPF requires knowledge at many different levels--there is what I call the "meta-level", in which styles, templates, and the like are applied. There is the "class-property level" of knowledge, which is essentially my background coming from declarative projects like MyXaml. And, there is the "work process level", which is knowledge in how to work with the Visual Studio WPF designer. There are other knowledge areas as well, for example, knowing the collection in which certain elements belong, knowing how to do data binding and event wire-up, knowing when to derive a class, or style it, or template it, or wire up an event without derivation. For the most part, these are all still mysteries to me.
The point of this exercise is to really begin at the beginning--what am I trying to do, what do I need to get WPF to do what I want, and why doesn't it work? So, for all you old hats at WPF, this will be very dull. If you are absolutely and completely new to WPF, have never read a WPF book (like me) and are trying to wrap your head around the excellent articles here on the Code Project (like me), then hopefully this article is right for you.
The Goal
What I'm trying to achieve is diagramming the relationships between SQL 2005/2008 tables. I've been poking around various diagramming tools, like Northwood's Go.Diagram, but nagging in the back of my head has been the idea of using WPF as a diagramming tool. Sukram's WPF Diagram Designer nudged me in that direction.
Requirements
I want to discuss just the visual requirements right now (not the data, or schema, requirements). The visual requirements are:
- Programmatically place an object onto the display surface
- Attach some text (the table name) to the object
- Allow the user to move the object around (for now, I am not planning on implementing any intelligent layout of tables)
- Draw lines (with an arrow head) between objects to designate the foreign key relationship; initially, these are direct lines, not "routed" lines which automatically route around objects
- Serialize/Deserialize (save and load) the diagram
- Provide zoom levels
- Provide auto-scrolling as required by the zoom level
- Provide an essentially infinite display surface on which to place and move objects
- Have the lines designating table relationships automatically adjust their centers when an object at the head or tail of the line is moved
In some ways, that's a tall order; however, it represents what I would consider to be the minimal application. Now, certainly I could start with sukram's excellent WPF Diagram Designer work, but I find myself so completely lost in trying to understand the WPF, that, even though I'm in many ways repeating, and in fact even setting the bar lower in terms of visual presentation, I do want to understand, from the beginning, how I code the above requirements with WPF.
Article Format
I've opted for a format in which I take each requirement (not necessarily in my original order) and illustrate how I met that requirement (in which, often, sub-requirements are discovered), followed by a brief commentary of things I learned when implementing that requirement.
Disclaimer
I am in no way attempting to create a comprehensive and complete discussion of XAML and WPF. These are the things that I've learned, and more importantly, chosen to learn about to a chosen depth of understanding. When learning a new technology, there is always a balance that must be struck between learning the technology and learning how to apply the technology to get the job done. I've found that everyone's balance point is different, and what you will implicitly encounter in this article is my balance point.
Programmatically Place an Object Onto the Display Surface
To do this, I first had to learn how objects are placed on the display surface.
Step 1: Create A WPF Project
Create a WPF project: launch VS2008, select File->New->Project, select "WPF Application", and call it "Lesson1".
Visual Studio creates some starting XAML for you:
<Window x:Class="Lesson1Redux.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>
</Grid>
</Window>
Now what?
What's a Window?
A System.Windows.Window
class is a container for managing content. It is derived from ContentControl
, which itself is derived from Control
and implements IAddChild
.
The IAddChild
interface is interesting in that, MSDN writes "For purposes of establishing or defining a content property or content model, IAddChild
is obsolete." Well, whatever. IAddChild
requires the implementor to provide the AddChild
and AddText
methods.
Also from MSDN: "Content Model: Window
is a ContentControl
, which means that Window
can contain content such as text, images, or panels. Also, Window
is a root element and, consequently, cannot be part of another element's content."
OK, so Window
provides two things:
- The standard frame for an application (title bar, system menu, border, size grip, minimize, maximize, and close buttons)
- It is a root element, the place where all things start
Next, we will see that a Window
also provides a Content
property, which is very useful.
What's a Grid and Do I Need it?
For example, I can delete the Grid
tags and, using the Toolbox, drop an Ellipse
onto the window, which yields:
<Window x:Class="Lesson1.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">
<Ellipse Height="100" Name="ellipse1" Stroke="Black" Width="200" />
</Window>
But, now, let's try dropping another item onto the window. It doesn't work! Looking at the Window
class, notice that it does not have a property for managing child controls. It has a "Content
" property. Conversely, if we look at the properties of a Grid
control, is has a Children
property.
Ah. Looking at the Content
property shows that it is indeed initialized to the Ellipse
instance. So, here I learned something--unlike the System.Windows.Forms
namespace, a Window
does not directly support child controls--instead, it provides a single property (Content
) which, if I want more than one control in the window, I have to use a "content control" that allows me to place multiple child controls into/onto it.
A Digression
Now, allow me to digress for a moment and critique the WPF authors, as it were. How do we know that a child element of a Window
class sets the Content
property? How do we know (as we'll see shortly) that a child element of a Canvas
class adds itself to the Children
collection property? What you have to do is look closely at the MSDN documentation, and the section that says "XAML Object Element Usage". If you see something like this:
followed by a discussion of the content model, especially the phrase "...enforces a strong content model for...", then you have what I believe is the indication that the default behavior, when the property is not supplied, is to initialize the child element indicated in the XAML Object Element Usage. Yes, it took me a while to figure that out.
Regardless, I feel this results in a confused, obfuscated, and frankly incorrect declarative syntax. Yes, I go so far as to say it is incorrect because the behavior of the declarative code becomes implicit as the result of how the parser is written or by some attribute of the metadata of the model, rather than explicit in providing all the information in a parser-agnostic manner to decode the markup.
Back to Business
The Grid
control is derived from Panel
, and from MSDN: "Panel
elements are components that control the rendering of elements". Ah. "...rendering of elements", plural. Derived Panel
controls designed specifically as root layout providers (meaning applications with a UI) are:
Canvas
DockPanel
Grid
StackPanel
VirtualizingStackPanel
WrapPanel
I suspect, for my requirements, the best suited panel would be a Canvas
, as I don't need wrapping, stacking, docking, or alignment to a grid, I just want freeform layout.
So, by changing the XAML:
<Window x:Class="Lesson1.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">
<Canvas>
<Ellipse Height="100" Name="ellipse1" Stroke="Black" Width="200" />
</Canvas>
</Window>
I note that the Content
property of the Window
is set to the Canvas
instance, and the Canvas.Children
property does indeed have an instance of the Ellipse
:
Positioning
To position an ellipse on the Canvas
, and I assume other controls as well, I have to use an attached property, in this case, Canvas.Top
and Canvas.Left
. By the way, if you have a Canvas.Bottom
and Canvas.Right
defined as well, the Top
and Left
properties take priority. So now, my markup inside the Canvas
block looks like:
<Ellipse Canvas.Left="108" Canvas.Top="122" Height="78"
Name="ellipse1" Stroke="Black" Width="116" />
<Ellipse Canvas.Left="27" Canvas.Top="24" Height="57"
Name="ellipse2" Stroke="Black" Width="91" />
Note, I added another ellipse. Now, I find my eyes glazing over when I read about attached properties, especially as they are entwined with dependency properties and the entire WPF model. So, I'm not going to delve deeper into attached properties. I hopefully know enough about setting the position of an object to muddle through the rest of this without an intimate understanding of attached properties.
Step 2: Programmatically Placing Objects
Now that I have some idea of drawing an object with the designer by dragging and dropping from the toolbox, how do I do this programmatically? There appear to be two choices--write all the code imperatively, like this:
public Window1()
{
InitializeComponent();
Canvas canvas = (Canvas)Content;
Ellipse ellipse = new Ellipse();
ellipse.Height = 78;
ellipse.Name = "ellipse1";
ellipse.Stroke = Brushes.Black;
ellipse.Width = 116;
Canvas.SetLeft(ellipse, 108);
Canvas.SetTop(ellipse, 122);
canvas.Children.Add(ellipse);
}
But, what if I want to use a XAML string? In my way of thinking, about the only thing XAML is useful for is creating the window, which is pretty lame. Can't I use XAML programmatically but declaratively? Well, there are two classes, XamlReader
and XamlWriter
, that appear to be usable for this purpose, although there are several notes in MSDN about these classes having a "series of limitations", which, of course, is not described. I guess I'll discover what they are along the way.
So, if I want to use XAML, I could instead do something like this:
public Window1()
{
InitializeComponent();
Canvas canvas = (Canvas)Content;
string xamlEllipse = "<Ellipse Height='78' Name='ellipse1' Stroke='Black' Width='116'
Canvas.Left='108' Canvas.Top='122'
xmlns="[text that won't render correctly on the article page]";
StringReader sr = new StringReader(xamlEllipse);
XmlReader xr = XmlReader.Create(sr);
Ellipse ellipse = (Ellipse)XamlReader.Load(xr);
canvas.Children.Add(ellipse);
}
which required adding the namespaces:
using System.IO;
using System.Windows.Markup;
using System.Xml;
Also note the xmlns
attribute, which is an "http" namespace, and is omitted here because it causes the article text to have fits. I figured out what the xmlns text should be by first serializing the Ellipse
created in the previous example and inspecting the resulting string. If I leave off the xmlns, the application crashes at runtime with the error:
XML namespace prefix does not map to a namespace URI,
so cannot resolve property 'Height'. Line '1' Position '10'.
Well, I guess that the error message explains why we need the xmlns
attribute.
Also note that I changed the double quote marks to single quote marks so I could more easily read the XAML as a string.
Isn't There a Better Way?
From my perspective, I really haven't improved the situation much. Isn't there a better way to programmatically place XAML controls that doesn't require imperative code or embedded XAML strings? I figure I'd look at sukram's WPF Diagram Designer to see how he does it. Unfortunately, he focuses on the issues with the connector rather than how an object, clearly defined in XAML, is dropped onto a Canvas
(effectively programmatically) when the user drags it from the toolbox to the Canvas
.
If you look at sukram's OnDrop
event in DesignerCanvas.cs, you'll note that he uses the XmlReader
class as well. Following this path backwards, it becomes obvious (in the ToolboxItem.cs file) that the XAML string is a serialization of the toolbox item that is itself being dragged onto the designer canvas. The XAML itself contains the markup for the toolbox, and therefore the process is a self-reflecting one, in which the toolbox item, having already been rendered in the toolbox, drops a copy (thanks to serialization) of itself onto the designer surface.
This is not quite what I want, as I don't have a starting toolbox. However, just by looking at the XAML code, one quickly realizes that sukram has created a very nice templated, styled, and resource driven environment, and it is the use of a ResourceDictionary
object that allows us to keep the markup for an object in XAML, and hopefully the ResourceDictionary
is accessible to us programmatically.
Step 3: The ResourceDictionary
So, let's tackle the harder problem first--how is the resource dictionary accessed programmatically? Well, we begin by defining the ResourceDictionary
in the XAML:
<Window x:Class="Lesson1Redux.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">
<Window.Resources>
<ResourceDictionary></ResourceDictionary>
</Window.Resources>
<Canvas>
</Canvas>
</Window>
Note the "Window.Resources
" element. There is a behavior in XAML that I personally dislike, and that is that a sub-element has a default, implicit property to which it is assigned, or if the property is a collection, to which it is added. As I stated above, a Window
can have only one Content
instance, and because Content
is an object, it can really be anything--certainly the parser can't use type information to say "hmm, a ResourceDictionary
should not be a Content
". And so, coupled with implicit property/collection assignment, I can't just write:
<Window x:Class="Lesson1Redux.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">
<ResourceDictionary/>
<Canvas/>
</Window>
No. This results in the error "The property Content cannot be set more than once". So instead, I have to tell the XAML parser explicitly which property I want set, and even worse (in my opinion) by having to reference the base class type. Incidentally, this:
<Window x:Class="Lesson1Redux.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">
<Window.Resources>
<ResourceDictionary/>
</Window.Resources>
<Window.Content>
<Canvas/>
</Window.Content>
</Window>
is also acceptable. But this:
<Window x:Class="Lesson1Redux.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">
<Resources>
<ResourceDictionary/>
</Resources>
<Content>
<Canvas/>
</Content>
</Window>
is not, resulting in the errors "The type Resources is not found" and "The type Content is not found". So, this gives us a glimmering into how properties, as child elements, must be set--with a "dotted notation" referencing the base class type and the desired property.
So, how do we add our XAML strings? Well, not as strings! The following markup describes the resource "Table
":
<Window.Resources>
<ResourceDictionary>
<Ellipse x:Key="Table" Height='78' Stroke='Black'
Width='116' Canvas.Left='108' Canvas.Top='122'/>
</ResourceDictionary>
</Window.Resources>
Above, you will note the attribute "x:Key
". This is an attribute you can apply when the parent container implements IDictionary
, in order to create a keyed collection.
And in the code, we can add it to the Canvas
Children
like this:
public Window1()
{
InitializeComponent();
Canvas canvas = (Canvas)Content;
canvas.Children.Add((UIElement)Resources["Table"]);
}
Now, here's some strange stuff. If you notice, the Ellipse
element above is missing the "Name
" attribute. If I include this attribute:
<Ellipse x:Key="Table" Name="Ellipse1" Height='78' Stroke='Black' Width='116'
Canvas.Left='108' Canvas.Top='122'/>
and I run the program in the debugger, I get this error:
If I single step through the program, it works.
I have successfully executed the above statement.
Are We Done?
Certainly not. The ResourceDictionary
is creating only a single instance, and if we want more than one "Table
" object drawn on our Canvas
, we are actually going to want to clone this beast. So, back to XamlReader
and XamlWriter
! But first, let's see what WPF does when I do try to add identical instances:
Sigh. What a sad state of affairs. The debugger doesn't even break. I get the above message. If I click on the OK button, I get:
Equally uninformative. Fortunately I'm smarter than WPF, and anticipated this problem, though I did not anticipate WPF handling this problem in such a brain dead manner. So, writing a simplistic cloner:
public Window1()
{
InitializeComponent();
Canvas canvas = (Canvas)Content;
canvas.Children.Add(Clone((UIElement)Resources["Table"]));
canvas.Children.Add(Clone((UIElement)Resources["Table"]));
Canvas.SetTop(canvas.Children[1], 20);
}
protected UIElement Clone(UIElement elem)
{
string str = XamlWriter.Save(elem);
StringReader sr = new StringReader(str);
XmlReader xr = XmlReader.Create(sr);
UIElement ret = (UIElement)XamlReader.Load(xr);
return ret;
}
The application runs. Oh, and what does it look like? I thought you'd never ask:
Notice how I am programmatically changing the Canvas.Top
attribute of the "second" ellipse.
Now, there are improvements to the ResourceDictionary
that can be made, such as referencing an external file and taking advantage of the ResourceDictionary.MergedDictionaries
property.
Commentary
A Window
requires a content manager to support multiple controls, but it is actually acceptable to provide a control if there is only one control that will be placed in the window.
There are several content providers, and the default Grid
class may not be the one I want.
I find it annoying that the property type of the Window.Content
property is "object
". One of the ways to learn about how a property can be used is by providing a type other than "object
", even if it's only an interface. Using "object
" is a hindrance to my learning process.
WPF's behavior regarding parsing XAML is quirky, at least in one instance, with regards to execution vs. single stepping through a program. If this is more common than the situation I found above, it must make debugging a WPF/XAML application rather difficult.
A general (based on one bad thing happening) conclusion: Is WPF lacking the "polish" that I have come to expect from the System.Windows.Forms
namespace classes with regards to exception and error management?
Working with the Visual Studio 2008 XAML designer, be forewarned that it is doggy.
Attach Some Text to the Object
It strikes me that just about every step of this process, and I've only addressed the first requirement, has been excruciatingly painful. I started this article (after several hours of playing with sukram's work) at about eight this morning. Barring breakfast, lunch, and grocery store trip, it is now 4:30 PM. And, I've only knocked off the first requirement!
Now, I want to add text to my ellipse, and again I am stumped, again I am Googling, reading CodeProject articles, blogs, MSDN, and the like. I am discovering that there is a certain non-sequitor logic to WPF. Things do not make sense. Things you would expect in an advanced environment like this are not there. Things that you would not expect in an advanced environment like this, well, guess what, they are there. In my years of learning frameworks, this is not to be chalked up to just "it's a new framework". I have a deep and disappointing feeling that this is because, in the final analysis, WPF is a rather bad framework. However, I see people doing amazing things with it, and so I will muddle along, and maybe one day, I will reread this article and will laugh at my ignorance and, dare I say, stupidity. I imagine the WPF'ers reading this section are rolling on the floor, thinking I'm an idiot. So be it.
So, how do I add or associate text with my ellipse? There is no "Text" property. One possible solution I tried was to create a TextBlock
as a child element:
<Canvas>
<Ellipse Height='78' Stroke='Black' Width='116' Canvas.Left='5' Canvas.Top='122'>
<TextBlock>Foobar</TextBlock>
</Ellipse>
</Canvas>
Sadly no, this does not work. The markup complains "The type 'Ellipse' does not support direct content". Well, that's a clue. I want "direct content". After some poking around, I thought, hmmm, maybe a TextBlock
can host an Ellipse
, and sure enough:
<Canvas>
<TextBlock Height='78' Width='116' Canvas.Left='5' Canvas.Top='122'>Foobar
<Ellipse Stroke="Black" Height="50" Width="50"/>
</TextBlock>
</Canvas>
Results in:
But, this isn't what I want either. It'd be nice to be able to position the text relative to the ellipse or other objects. Perhaps, I want the text in the center, or centered at the top or bottom. It strikes me that my whole approach here is wrong, and what I might want is something that is a composite control consisting of the text, the FrameworkElement
object (like an Ellipse
), and an enumeration describing where the text goes.
Well, let us compromise for the moment. If I eliminate the requirement to place the text in the center (which is of dubious use anyways), then I can use a StackPanel
to place the text either above or below the ellipse. If I code this in XAML:
<Canvas>
<StackPanel Canvas.Left="10" Canvas.Top="10" >
<TextBlock TextAlignment="Center">Foobar</TextBlock>
<Ellipse Stroke="Black" Height="50" Width="100"/>
</StackPanel>
</Canvas>
I get:
Note that I don't have to provide the StackPanel
with a width and height--it's default behavior is to stretch to the extents of the child elements.
But, I can't find anything in the properties of the StackPanel
to say, stack this top-to-bottom or bottom-to-top! You will find it amusing though that the StackPanel
offers a right-to-left and left-to-right option when setting the FlowDirection
property to Horizontal
. I suppose in the WPF designers' minds, the concept of "stack" implies top-down, even though in the real world things are stacked bottom up. So for now, I'm going to concede the point entirely and simply put the text at the bottom.
Now, the other thing I'm going to do is move the StackPanel
markup into my ResourceDictionary
.
Commentary
I would like to be able to:
- move the text relative to the
FrameworkElement
object in some fashion other than what is offered to me in a StackPanel
- "neatly" describe the dimensions of the object
- "neatly" set the text
And by "neatly", I mean by setting a property directly and easily, without drilling into the Canvas
and StackPanel
child collections, and so forth. In other words, I'd like to be able to decouple the model implementation from the ability to assign values to common model properties such as position, size, and label.
The Complete Code
This is pretty unimpressive for an article of this length, and hence is why there is no code download for this article.
The XAML code:
<Window x:Class="Lesson1Redux.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">
<Window.Resources>
<ResourceDictionary>
<StackPanel x:Key="Table" Canvas.Left="10" Canvas.Top="10">
<Ellipse Stroke="Black" Height="50" Width="100"/>
<TextBlock TextAlignment="Center">Foobar</TextBlock>
</StackPanel>
</ResourceDictionary>
</Window.Resources>
<Canvas/>
</Window>
And the imperative code that programmatically creates two of these stacks:
public Window1()
{
InitializeComponent();
Canvas canvas = (Canvas)Content;
canvas.Children.Add(Clone((UIElement)Resources["Table"]));
Canvas.SetTop(canvas.Children[0], 10);
canvas.Children.Add(Clone((UIElement)Resources["Table"]));
Canvas.SetTop(canvas.Children[1], 100);
}
protected UIElement Clone(UIElement elem)
{
string str = XamlWriter.Save(elem);
StringReader sr = new StringReader(str);
XmlReader xr = XmlReader.Create(sr);
UIElement ret = (UIElement)XamlReader.Load(xr);
return ret;
}
Resulting in:
Conclusion
I've only gotten to the first two of my requirements, and already this article is long enough for my tastes. It was pretty clear to me that writing about WPF often results in long articles, and now I see why! In fact, neither of these two requirements is implemented to my satisfaction, so I will be revisiting them as I learn more.
Also, obviously, this is only part one, as I've only accomplished the first two tasks of my requirements, and I'm not exactly thrilled with the way I had to compromise on my second task.
So, all you WPF experts out there--tell me how I could do things better!
References