Contents
I was taking a look at Joe Marini's Blog Reader Built in XAML, and I thought, why wait for Longhorn, I can do that with MyXaml! But it wasn't that easy, as I needed to implement some features in MyXaml that didn't yet exist, and even worse, implement an owner draw list box so I could get that nice multi-line rendering of blog titles. Still, it didn't take more than a day or so to make the necessary modifications, and the results are very nice:
If you aren't already familiar with MyXaml, you can read my original article on it here, or visit my website http://www.myxaml.com/. Basically, MyXaml is a general class instantiator capable of initializing properties, wiring up events to event handlers, and drilling down into property collections, customizable with inline or code-behind, language non-specific, JIT assembling at runtime, all driven by XML markup. This means that with MyXaml:
- You can instantiate presentation layer components such as forms and controls through markup rather than code.
- You can initialize properties like color, state, captions, and collections.
- You can write in-line code that is assembled and instantiated along with the form--handle events, initialize data sets, and so forth. You can write this code in whatever language is supported by .NET.
- You can tie in to compile-time code also.
- And, nothing says you are limited to doing this with presentation layer components. Want to instantiate a connection to a database specified using markup? No problem.
Implementing the reader required creating a few classes to help .NET along.
First, a XmlDataSource
class with a custom type converter to convert a node list returned from an XPath statement to a DataTable
is needed.
Second, a DataSource
class with a custom type converter to convert a DataTable
to a System.Object
is needed. Why? Because the DataSource
property of a ListBox
takes a System.Object
type, and we need to tell the parser that it can specifically convert a MyXaml.Extensions.DataSource
instance to a System.Object
type, which actually returns the DataTable
instance contained in the DataSource
class. And we do that because we can't directly instantiate a meaningful DataTable
instance.
An owner draw ListBox
is required because natively a list box only displays one line per item, and I needed something that displayed multiple lines--the caption, date, and URL of the blog entry, plus doing that cool gradient background fill.
And finally, a custom Link
handler because the way .NET's LinkLabel
works is not exactly simple.
First, let's look at the elements of the XML header:
="1.0"="utf-8"
<MyXaml xmlns="System.Windows.Forms"
xmlns:def="Definition">
The first line describes the xml
markup version and encoding format. That's straightforward enough.
The second line defines the root node (MyXaml
), and establishes that two namespaces are being used. The first is the default namespace (as there is no prefix), and is assigned to the System.Windows.Forms
namespace. In MyXaml, an assembly can be identified using a partial name (this is a fairly new feature). I specifically chose not to implement the CLR namespace to xml
namespace mapping that you'll find in the Longhorn XAML references because I just didn't think it was necessary.
The MyXamlLoader
, a utility for automatically searching and loading files with the .myxaml extension, always expects the application's form to be identified with the "AppMainForm
" tag:
<Form Name='AppMainForm'
ClientSize='800, 600'
StartPosition='CenterScreen'
Text='MyXaml RSS Reader Demonstration'>
As you can see, four properties of the Form
instance are being initialized.
Clicking on a LinkLabel
does not automatically invoke the browser. There are two places that a link can be clicked in the blog reader--the link in the header, and the link for each blog entry. To invoke the browser, a little inline code is required:
<def:Code language='C#'>
<reference assembly="System.Windows.Forms.dll"/>
<reference assembly="myxaml.dll"/>
<![CDATA[
using System;
using System.Windows.Forms;
using MyXaml;
using MyXaml.Extensions;
class Helpers
{
public void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
LinkLabel ll=(LinkLabel)sender;
int idx=ll.Links.IndexOf(e.Link);
ll.Links[idx].Visited=true;
System.Diagnostics.Process.Start((string)e.Link.LinkData);
}
public void OnGotoBlogEntry(object sender, EventArgs e)
{
StyledListBox slb=(StyledListBox)sender;
string url=slb.SubItem(2);
System.Diagnostics.Process.Start(url);
}
}
]]>
</def:Code>
In MyXaml, there are two forms of "code-behind". One is the example just illustrated: in-line code built directly into the xml markup. The second is a reference to a separate code file. In addition to this, the event target of any events that MyXaml needs to wire-up can be specified when invoking the parser or by adding a target to the event target collection. Since we're using the generic loader, and there are no other assemblies being used, the only option is inline or a separate code file.
MyXaml maintains a stack of event targets. Each xml element can have its own inline or separate code file, and any class instantiated in that code are local to that element and its children. MyXaml will automatically instantiate all classes with a parameter-less constructor that it finds in the in-line or code file. As long as you use method names for the event handlers that are unique, you don't need to tell MyXaml to which instance to wire up the event--the parser figures it out automatically. If you do need to tell MyXaml the class instance, you can prefix the event's method name with the class name followed by a dot ('.') in the markup.
The fun part begins now. As mentioned earlier, MyXaml is a general purpose instantiator. It isn't restricted to certain CLR objects and it doesn't implement any custom parsing (except to get around some .NET limitations where necessary). There are a few basic rules to creating a MyXaml-friendly assembly, which I've written about here.
The first thing that really happens is that the XmlDataSource
is instantiated:
<XmlDataSource
def:Name="NewsData"
Source="<a href="http://msdn.microsoft.com/rss.xml">http://msdn.microsoft.com/rss.xml</a>"
XPath="rss/channel"/>
Here, the Name
attribute has a "def:
" prefix, which tells MyXaml to add the object to an object collection that can be referenced elsewhere. Late binding is implemented because MyXaml first instantiates all tags, drilling down into child nodes, then, as the stack is popped, the attributes for each tag are assigned.
The Source
attribute specifies the xml source, in this case hardcoded to the MSDN website.
The XPath attribute specifies the base path with qualifies all other sub-paths used later on. Remember, this format is for RSS 2.0.
The next step is to extract the blog entries. This is accomplished by creating a MyXaml.Extensions.DataSource
object:
<DataSource def:Name="Items" Source="{NewsData; Path=item}"/>
Here, the Source
attribute specifies the XmlDataSource
object (note the use of the {}
to reference objects in the global object collection). Additional qualifiers can be passed to the referenced object which typically affects the data that is being returned. In this particular case, the "Path
" field provides additional qualifiers to the XPath base path defined earlier.
The implementation for the XmlDataSource
needs to figure out a view things. First off, it has to implement a custom type converter, which is accomplished using a TypeConverter
attribute:
namespace MyXaml.Extensions
{
[TypeConverter(typeof(XmlDataSourceTypeConverter))]
public class XmlDataSource
{
...
The parser checks to see if the referenced object can be converted to the type expected by the attribute. In this case, the XmlDataSource
needs to be converted to a DataTable
type, because this is how the MyXaml.Extensions.DataSource
class defines the "Source
" property:
namespace MyXaml.Extensions
{
[TypeConverter(typeof(DataSourceTypeConverter))]
public class DataSource
{
protected DataTable source;
protected string field;
public DataTable Source
{
get {return source;}
set {source=value;}
}
...
Going back to the XmlDataSource
class, the type converter implements two methods: CanConvertTo
and ConvertTo
.
public class XmlDataSourceTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type t)
{
if (t.FullName=="System.String") return true;
if (t.FullName=="System.Data.DataTable") return true;
return false;
}
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value,
Type destinationType)
{
object ret=null;
XmlDataSource xds=(XmlDataSource)value;
if (destinationType.FullName=="System.Data.DataTable")
{
ret=xds.LoadDataTable();
}
else
{
ret=xds.ToString();
}
return ret;
}
As you can, depending on the target type (a String
or a DataTable
), the converter does something different!
Once the xml data source has been created and a DataTable
is available, we can start working with displaying the blog entries. By leveraging the ListBox
data binding feature, we can use .NET to automatically update the description whenever an item is selected in the list. This markup does just that:
<Panel Dock="Fill" BackColor="White">
<Controls>
<TextBox Multiline="true" Dock="Fill" Font="MS Sans Serif, 12pt">
<DataBindings>Text, Items, description</DataBindings>
</TextBox>
</Controls>
</Panel>
It instantiates a Panel
containing a TextBox
. The TextBox
's Text property is bound to the "description
" field of the Items
collection. In order to accomplish this, the DataSource
class needs a type converter so that the contained DataTable
is returned when the DataSource
object is being referenced.
Similarly, the blog caption is parsed directly from the XmlDataSource
. Because we're interested in a single value return (String
objects), the type converter for XmlDataSource
expects that the Path
qualifier results in a query that returns a single node.
<Panel Dock="Top" Height="100" BackColor="LightGray">
<Controls>
<Label Location="0, 0" Size="800, 40"
Font="MS Sans Serif, 24pt, style=Bold"
Text="{NewsData; Path=title}"/>
<Label Location="0, 40" Size="800, 40"
Font="MS Sans Serif, 12pt"
Text="{NewsData; Path=description}"/>
<LinkLabel Location="0, 80" Size="800, 20"
Font="MS Sans Serif, 10pt"
Text="{NewsData; Path=link}" LinkClicked="OnLinkClicked">
<Links>
<Link LinkData="{NewsData; Path=link}"/>
</Links>
</LinkLabel>
</Controls>
</Panel>
Here, the Text
is being assigned to the XmlDataSource
object qualified by the designated Path
values, and the type converter that converts the XmlDataSource
to String
does the real work:
public override string ToString()
{
string ret=String.Empty;
XmlNodeList nodes;
try
{
nodes=document.SelectNodes(xpath+"/"+path);
if (nodes.Count==1)
{
ret=nodes[0].InnerText;
}
}
catch(Exception e)
{
Trace.WriteLine(e.Message);
}
return ret;
}
The final and most interesting part is the owner draw ListBox
.
<StyledListBox Dock="Left" Width="390" BackColor="White"
BorderStyle="Fixed3D" DataSource="{Items}" VisualStyle="Entries"
Margin="10" DoubleClick="OnGotoBlogEntry"/>
Again, the type converter that takes a MyXaml.Extensions.DataSource
instance and converts it to a DataTable
instance is being utilized, this time in the </ODE>DataSource
attribute of the ListBox
.
The VisualStyle
attribute is my poor man's way of implementing Longhorn's VisualTree
(which, in my opinion, is an abomination of custom parsing that breaks the generic nature of the markup parser). In any case, this attribute tells the owner draw control how exactly it should draw each item. The "Entries
" are defined just like any other controls:
<Panel Name="Entries">
<Controls>
<TextBox Font="MS Sans Serif, 12pt"
Text="Items; Field=title" WordWrap="true"/>
<TextBox Font="MS Sans Serif, 9pt"
Text="Items; Field=pubDate" ForeColor="Gray" WordWrap="true"/>
<TextBox Font="MS Sans Serif, 9pt"
Text="Items; Field=link" WordWrap="false"/>
</Controls>
</Panel>
Note that the Text
attribute here does not use the {}
to reference an object. If it did, the reference would be resolved when the owner drawn ListBox
instantiates the controls! Rather, the owner drawn ListBox
"knows" that the Text
property is going to be referencing a DataTable
and resolves the reference internally. The syntax stays the same for consistency, but without the {}
.
protected void GetText(Graphics gr, Control ctrl, int index, out string text,
out int height)
{
text=ctrl.Text;
height=0;
bool wordWrap=false;
wordWrap=((TextBox)ctrl).WordWrap;
string dataSourceName=StringHelpers.LeftOf(text, ';').Trim();
string field=StringHelpers.RightOf(text, '=').Trim();
Parser parser=Parser.CurrentInstance;
object ds=parser.GetReference(dataSourceName);
DataTable dt=null;
TypeConverter targetTypeConverter=TypeDescriptor.GetConverter(ds.GetType());
if (targetTypeConverter.CanConvertTo(typeof(DataTable)))
{
dt=(DataTable)targetTypeConverter.ConvertTo(ds, typeof(DataTable));
}
if (dt != null)
{
text=(string)dt.Rows[index][field];
SizeF sf;
if (wordWrap)
{
sf=gr.MeasureString(text, ctrl.Font, Width-20);
}
else
{
sf=gr.MeasureString(text, ctrl.Font);
}
height=(int)sf.Height+1;
}
}
As I mentioned, this is a poor man's implementation of a VisualTree
. .NET doesn't have the ability for controls to render themselves on any surface (this is why we need Longhorn!) and therefore the implementation of the StyledListBox
is limited to parsing TextBox
visual style elements. Of course, it could be extended to other types, such as checkboxes, etc., if these were owner drawn as well. But still, it's pretty sophisticated, in that it measures the amount of space that each item requires.
That's it! The entire reader is implemented in xml markup with a couple helper functions declared in-line. The XmlDataSource
, DataSource
, and StyledListBox
classes, while rudimentary in their implementation, are the beginnings of some nice generic extensions. The whole thing shows the flexibility of implementing interfaces with an eXtensible Application Markup Language (XAML), or was that Xml Application Markup Language?
To see some more interesting examples (such as reading resources), check out the MyXaml Designer, downloadable at http://www.myxaml.com/ (screen shots here). Unfortunately, the source is not available because it's written using the Actipro Software syntax editor, which is licensed per developer seat. But the MyXaml markup is viewable and demonstrates Resources and a couple other nifty things you can do with MyXaml.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.