This post presents a little unfinished experiment for creating an
XSD Xml Schema definition from classes based on some of the rules to map
XAML documents to objects. The program is written in
IronRuby and it uses reflection to inspect classes and generate the schema definition.
The goal is to have an XML Schema that could be used in conjunction with an XML editor to create XAML documents (which is useful for those of us who don't have a full Visual Studio version). Although as the
XAML Overview document says, there are elements that could not be completely mapped to an schema definition, some of them are mentioned below.
The
Silverlight Visual Studio integration already includes a very nice XAML editing capabilities.
I think this experiment is a great way to learn more about IronRuby and how to use it to call .NET Libraries.
The strategyWhat the program will do is to navigate all the classes inheriting from
System.Windows.DependencyObject and generate and XML element and a
complex type definition with all the properties included in the definition. For this experiment only two mappings are implemented: properties and content properties.
PropertiesAs the
XAML Overview document describes two ways for specifying properties:
- By using XML attributes
- By using a class.property-name element
This means that:
This
<Button Background="Blue" >
...
</Button>
and
<Button>
<Button.Background>
<SolidColorBrush Color="Blue">
</Button.Background>
...
</Button>
are equivalent.
So the alternative is to create both the attribute and the property element definitions in the schema.
Content propertiesFor elements that contain the
ContentPropertyAttribute a special child element will be created with a reference to a sequence of all identified concrete elements that in inherit from the property type. As discussed below, this definition is not complete for content properties that accept basic types such as a string.
The programSome constant and variable definitions
require 'mscorlib'
require 'System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL'
include System::Xml
include System::Reflection
SILVERLIGHT_FOLDER = "c:\\Program files\\Microsoft Silverlight\\2.0.30523.8\\"
BASE_TYPE_NAME = "System.Windows.DependencyObject"
CONTENT_PROPERTY_ATTRIBUTE = "System.Windows.Markup.ContentPropertyAttribute"
SILVERLIGHT_NAMESPACE = "http://schemas.microsoft.com/client/2007"
EXTRA_ATTRIBUTES_NAMESPACE = "http://schemas.microsoft.com/winfx/2006/xaml"
XSD_NAMESPACE = "http://www.w3.org/2001/XMLSchema"
CONCRETE_ELEMENTS_GROUP_NAME = "UIElementsGroup"
PRESENTATION_FRAMEWORK_COLLECTION_BASE_TYPE = "PresentationFrameworkCollection`1"
The main programThe main program looks like this:
begin
silveright_system_assembly = Assembly.reflection_only_load_from(SILVERLIGHT_FOLDER + "system.dll")
silveright_windows_assembly = Assembly.reflection_only_load_from(SILVERLIGHT_FOLDER + "System.Windows.dll")
silveright_core_assembly = Assembly.reflection_only_load_from(SILVERLIGHT_FOLDER + "System.Core.dll")
silveright_net_assembly = Assembly.reflection_only_load_from(SILVERLIGHT_FOLDER + "System.Net.dll")
silveright_xml_assembly = Assembly.reflection_only_load_from(SILVERLIGHT_FOLDER + "System.Xml.dll")
registry = Registry.new(silveright_windows_assembly)
registry.collect_data
registry.generate_xsd_schema
puts 'Done!'
rescue System::Reflection::ReflectionTypeLoadException => tl
puts tl
puts tl.LoaderExceptions
rescue System::IO::FileLoadException => e
puts e
puts "-----"
puts e.FusionLog
end
As presented here, the program first collects data about the classes stored in the
System.Windows.dll
Silverlight library and then generate the XSD schema definition.
The Registry classThe
Registry
class stores information on the identified classes and keeps track of element groups to be generated.
The
collect_data
method of the
Registry
class looks like this:
class Registry
def initialize(types_assembly)
@groups = {}
@types_assembly = types_assembly
@additional_types = {}
@classes = {}
end
...
def get_or_create(name,registry)
if @classes.has_key? name
return @classes[name]
else
return (@classes[name] = ClassNode.new(name,nil,registry))
end
end
...
def collect_data
base_type = @types_assembly.GetType(BASE_TYPE_NAME)
@classes[base_type.FullName] = RootClassNode.new(BASE_TYPE_NAME,base_type,self)
@types_assembly.get_types.each do |a_type|
if (base_type.is_assignable_from a_type and base_type.FullName != a_type.FullName )
node = get_or_create(a_type.full_name,self)
node.the_type = a_type
parent = get_or_create(a_type.BaseType.full_name,self)
parent.add_child(node)
puts "Adding #{a_type.FullName}"
end
end
end
end
As shown here the
collect_data
method iterates all the classes in the assembly, asking for elements that descend from
System.Windows.DependencyObject. For each of these classes an instance of the
ClassNode
class is created.
If you are familiar with the .NET Reflection API you will recognize some of the names presented here such as
is_assignable_from
which is a call to the
IsAssignableFrom method. As described
here, IronRuby allows you to call existing .NET method names using Ruby naming convention .
Generating the SchemaThe XSD schema is generated in the
generate_xsd_schema
Registry method which looks like this:
def generate_xsd_schema
base_type = @types_assembly.GetType(BASE_TYPE_NAME)
swriter = System::IO::StreamWriter.new("silveright.xsd")
writer_settings = XmlWriterSettings.new()
writer_settings.Indent = true
w = XmlWriter.Create(swriter,writer_settings)
w.write_start_document
w.write_start_element("schema",XSD_NAMESPACE)
w.write_attribute_string("targetNamespace",SILVERLIGHT_NAMESPACE)
w.write_attribute_string("elementFormDefault","qualified")
w.write_attribute_string("xmlns","sl",nil,SILVERLIGHT_NAMESPACE)
w.write_attribute_string("xmlns","x",nil,EXTRA_ATTRIBUTES_NAMESPACE)
w.write_start_element("import",XSD_NAMESPACE)
w.write_attribute_string("namespace",EXTRA_ATTRIBUTES_NAMESPACE)
w.write_attribute_string("schemaLocation","extraxamldefs.xsd")
w.write_end_element()
@classes[base_type.FullName].write_schema_definition(w)
create_additional_type_definitions(w)
create_concrete_elements_group(w)
w.write_end_element
w.write_end_document
w.Close
swriter.Close
end
As shown here a .NET
XmlWriter class is used to generate the schema.
The
write_schema_definition
of the
RootClassNode
and
ClassNode
classes generates all the appropriate definitions for each class.
For the
RootClassNode
which represents classes that don't inherit from the
DependencyObject
the code looks like this:
class RootClassNode
attr_accessor :name,:the_type,:children
def initialize(name,the_type,registry)
@the_type = the_type
@name = name
@children = {}
@registry = registry
end
...
def write_schema_definition(writer)
ctype_name = @the_type.Name.to_s+"Type"
writer.write_start_element("complexType",XSD_NAMESPACE)
writer.write_attribute_string("name",ctype_name )
write_inner_elements_definition(writer) unless is_abstract
write_properties_definition(writer)
if (@the_type.FullName.to_s == BASE_TYPE_NAME)
writer.write_start_element("attributeGroup",XSD_NAMESPACE)
writer.write_attribute_string("ref","x:extraAttributes")
writer.write_end_element
end
writer.write_end_element
write_element_definition(writer,ctype_name) unless is_abstract
@children.values.each {|c| c.write_schema_definition(writer)}
end
end
A
complex type is generated with the content of the current class. The
write_inner_elements_definition
method writes all the description of the child nodes for this complexType, for example it writes the property/element definitions and child node references. The
write_properties_definition
method writes the
attribute definitions for all the properties.
Also for all base types, a reference to an attribute group of "extraAttributes" is generated. This attribute group contains reference to definitions for some of the XAML attributes such as
x:Name
. More information about these attributes can be found in
XAML Namespace (x:) Language Features.
Finally an
element definition is created if the class is not abstract.
For classes inheriting from DependencyObject, a
ClassNode
instance is created.
class ClassNode < RootClassNode
def write_schema_definition(writer)
if @the_type.contains_generic_parameters
ctype_name = @the_type.Name.to_s.gsub(/`/,'')+"Type"
else
ctype_name = @the_type.Name.to_s + "Type"
end
writer.write_start_element("complexType",XSD_NAMESPACE)
writer.write_attribute_string("name",ctype_name )
writer.write_start_element("complexContent",XSD_NAMESPACE)
writer.write_start_element("extension",XSD_NAMESPACE)
writer.write_attribute_string("base","sl:#{@the_type.BaseType.Name}Type")
write_inner_elements_definition(writer) unless is_abstract
write_properties_definition(writer)
writer.write_end_element
writer.write_end_element
writer.write_end_element
write_element_definition(writer,ctype_name) unless is_abstract
@children.values.each {|c| c.write_schema_definition(writer)}
end
end
The main difference with
BaseClassNode
is that a
complex type extension to the base type is generated. This will reduce the number of attribute definitions of each complex type.
Writing child node referencesIn order to allow sequences of heterogeneous elements as child nodes of XAML elements, a
group definition is created with a
choice that references every concrete type.
For example for elements that have a content property of type UIElement the following group is generated:
<group name="UIElementGroup">
<choice>
<element ref="sl:Path" />
<element ref="sl:Ellipse" />
<element ref="sl:Line" />
<element ref="sl:Polygon" />
<element ref="sl:Polyline" />
<element ref="sl:Rectangle" />
...
</choice>
</group>
Combining Enum valuesThe only way, that I could find, for combining .NET Enum values was to use a combination of
Convert.ToInt32 and
Enum.ToObject. The following function was used to do that.
def combine(enum_type,enum_values)
System::Enum.ToObject(
enum_type.to_clr_type,
((enum_values.map {|e_value|
System::Convert.ToInt32(e_value)}).inject {|i,j| (j | i)}))
end
A use of this function for combining
BindingFlags looks like this:
p = @the_type.get_property(
c.to_string,
combine(BindingFlags,
[BindingFlags.Instance,
BindingFlags.Public,
BindingFlags.NonPublic]))
result_type = p.PropertyType
Elements not mappedAs mentioned at the beginning of the document, not all XAML document features can be accurately represented using XSD. Some of the things that I noticed:
- Couldn't find a way to define attached properties. A possible workaround is to generate all possible attached property definitions
- Content properties that allow strings are not represented. This is difficult since it has conflicts with the property/element definitions. Mixed content could be a possible workaround.
- Extensibility: no easy way to represent things outside of System.Windows. This is a very difficult problem, maybe things like substitution groups could help to represent future child nodes.
Using the generated schemaWith the schema generated, an XML editor with XSD Schema aware completion can be used. For example here it is used in
Eclipse with XML editor tools included in
WTP.
I tried to use the schema with the
Netbeans 6.1 IDE (which was used as the Ruby editor for this code) however for schema completion
it requires you to specify the
schemaLocation attribute. Using this attribute or declaring the
xsi namespace generates an error when the XAML is loaded at runtime!.
Code and generated schema for this post can be found
here.