Joshua Nussbaum
2008-Dec-12 06:55 UTC
[Ironruby-core] WPF databinding with ruby objects. Workaround :)
Hey yall
Seems IronRuby objects do not yet work with WPF databinding. One approach to
a workaround is to emit a wrapper type that defines CLR properties. When WPF
calls the getter/setter the call is forwarded ot the contained contained
ruby object. This will allow defining business objects in ruby and databing
them to the UI.
This might be useful to thers, so here is my approach. The first part is C#
code that defines a type given a list of property names. The second part is
ruby code that creates a wrapper instance for a given ruby object and caches
the type so that you dont generate a type for each element in an array..
The C# code to generate a type:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
namespace GenerateType
{
public class TypeGenerator
{
public delegate object GetPropertyDelegate(string propertyName);
public delegate object SetPropertyDelegate(string propertyName,
object value);
public static Type Generate(string className, List<string>
properties)
{
AssemblyName asmName = new AssemblyName("BindingTypes");
AssemblyBuilder asmBuilder
AppDomain.CurrentDomain.DefineDynamicAssembly(asmName,
AssemblyBuilderAccess.Run);
ModuleBuilder modBuilder
asmBuilder.DefineDynamicModule("Types");
TypeBuilder typeBuilder = modBuilder.DefineType(className,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout);
FieldBuilder getFieldBuilder =
typeBuilder.DefineField("OnGet",
typeof(GetPropertyDelegate), FieldAttributes.Public);
FieldBuilder setFieldBuilder =
typeBuilder.DefineField("OnSet",
typeof(SetPropertyDelegate), FieldAttributes.Public);
MethodAttributes getSetAttr
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.HideBySig;
foreach (string propertyName in properties)
{
PropertyBuilder propBuilder
typeBuilder.DefineProperty(propertyName, PropertyAttributes.None,
typeof(object), new Type[] {});
MethodBuilder getter
typeBuilder.DefineMethod("get_" +
propertyName,
getSetAttr,
typeof(string),
Type.EmptyTypes);
ILGenerator ilGen = getter.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, getFieldBuilder);
ilGen.Emit(OpCodes.Ldstr, propertyName);
ilGen.Emit(OpCodes.Callvirt,
typeof(GetPropertyDelegate).GetMethod("Invoke"));
ilGen.Emit(OpCodes.Ret);
// Define the "set" accessor method for CustomerName.
MethodBuilder setter
typeBuilder.DefineMethod("set_" + propertyName,
getSetAttr,
null,
new Type[] { typeof(string)
});
ilGen = setter.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, setFieldBuilder);
ilGen.Emit(OpCodes.Ldstr, propertyName);
ilGen.Emit(OpCodes.Ldarg_1);
ilGen.Emit(OpCodes.Callvirt,
typeof(SetPropertyDelegate).GetMethod("Invoke"));
ilGen.Emit(OpCodes.Pop);
ilGen.Emit(OpCodes.Ret);
// Last, we must map the two methods created above to our
PropertyBuilder to
// their corresponding behaviors, "get" and
"set"
respectively.
propBuilder.SetGetMethod(getter);
propBuilder.SetSetMethod(setter);
}
return typeBuilder.CreateType();
}
}
}
And here is the ruby code to generate the wrapper type
require ''mscorlib''
require ''C:\Documents and Settings\Josh\My Documents\Visual Studio
2008\Projects\GenerateType\GenerateType\bin\Release\GenerateType.dll''
require ''PresentationFramework, Version=3.0.0.0,Culture=neutral,
PublicKeyToken=31bf3856ad364e35''
require ''PresentationCore, Version=3.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35''
require ''WindowsBase, Version=3.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35''
include System
include System::Collections::Generic
include GenerateType
include System::Collections::ObjectModel
include System::Windows
include System::Windows::Controls
include System::Windows::Data
include System::Windows::Input
class WrapperGenerator
def initialize
@wrapper_cache = {}
end
def wrap(ruby_object)
if ruby_object.is_a? Array
ruby_object.map {|o| wrap(o) }
else
cache(ruby_object) unless cached(ruby_object)
wrapper_class = cached(ruby_object)
wrapper_class.new(ruby_object)
end
end
def invalidate
@wrapper_cache.clear
end
private
def cached(object)
@wrapper_cache[object.class.name]
end
def cache(object)
@wrapper_cache[object.class.name] = generate_wrapper(object)
end
def generate_wrapper(object)
wrapper_name = "#{object.class.name}Wrapper"
properties = List.of(System::String).new
(object.methods - Object.instance_methods).each {|m| properties.add
m.to_s}
wrapper_base_type = TypeGenerator.generate("#{wrapper_name}Base",
properties)
base_instance = Activator.create_instance wrapper_base_type
eval <<EOS
class #{wrapper_name} < base_instance.class
def initialize(original)
self.on_get = lambda do |prop|
original.send prop
end
self.on_set = lambda do |prop, val|
original.send "\#{prop}=", val
end
end
end
return #{wrapper_name} # return the class
EOS
end
end
class Person
attr_accessor :first_name, :last_name
def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
def full_name
"#{last_name}, #{first_name}"
end
end
# A sample UI
people = []
10.times { |n| people << Person.new("John #{n}",
"Smith") }
wrapper = WrapperGenerator.new
w = Window.new
combo = ComboBox.new
combo.items_source = wrapper.wrap(people) # wrap people objects
combo.display_member_path = "full_name"
combo.margin = 20
w.content = combo
Application.new.run w
-------------- next part --------------
An HTML attachment was scrubbed...
URL:
<http://rubyforge.org/pipermail/ironruby-core/attachments/20081212/77fd377c/attachment-0001.html>
Robert Brotherus
2008-Dec-12 12:32 UTC
[Ironruby-core] WPF databinding with ruby objects. Workaround :)
Great work Joshua!
IronRuby-objects don''t work in many parts of dotnet-framework because
they are instances of Ruby-classes that are not dotnet-classes. Another
example that has causes headache to me is XAML where names of
user-defined classes can be used as elements but they must be
user-defined dotnet-classes (not Ruby).
I suppose and hope that with the support for dynamic languages
increasing in dotnet 4 / C# 4, there will be more built-in support for
this (it would be really nice to use dynamic types as XAML-elements...)
Robert Brotherus
Software architect
Napa Ltd
Tammasaarenkatu 3, Helsinki FI-00180
P.O.Box 470, Helsinki FI-00181
Tel. +358 9 22 813 1
Direct. +358 9 22 813 611
GSM +358 45 11 456 02
Fax. +358 9 22 813 800
Email: Robert.Brotherus at napa.fi <mailto:Robert.Brotherus at napa.fi>
www.napa.fi <http://www.napa.fi/>
_____
From: ironruby-core-bounces at rubyforge.org
[mailto:ironruby-core-bounces at rubyforge.org] On Behalf Of Joshua
Nussbaum
Sent: Friday, December 12, 2008 8:56 AM
To: ironruby-core at rubyforge.org
Subject: [Ironruby-core] WPF databinding with ruby objects. Workaround
:)
Hey yall
Seems IronRuby objects do not yet work with WPF databinding. One
approach to a workaround is to emit a wrapper type that defines CLR
properties. When WPF calls the getter/setter the call is forwarded ot
the contained contained ruby object. This will allow defining business
objects in ruby and databing them to the UI.
This might be useful to thers, so here is my approach. The first part is
C# code that defines a type given a list of property names. The second
part is ruby code that creates a wrapper instance for a given ruby
object and caches the type so that you dont generate a type for each
element in an array..
The C# code to generate a type:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
namespace GenerateType
{
public class TypeGenerator
{
public delegate object GetPropertyDelegate(string propertyName);
public delegate object SetPropertyDelegate(string propertyName,
object value);
public static Type Generate(string className, List<string>
properties)
{
AssemblyName asmName = new AssemblyName("BindingTypes");
AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.
DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
ModuleBuilder modBuilder
asmBuilder.DefineDynamicModule("Types");
TypeBuilder typeBuilder = modBuilder.DefineType(className,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout);
FieldBuilder getFieldBuilder
typeBuilder.DefineField("OnGet", typeof(GetPropertyDelegate),
FieldAttributes.Public);
FieldBuilder setFieldBuilder
typeBuilder.DefineField("OnSet", typeof(SetPropertyDelegate),
FieldAttributes.Public);
MethodAttributes getSetAttr
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.HideBySig;
foreach (string propertyName in properties)
{
PropertyBuilder propBuilder
typeBuilder.DefineProperty(propertyName, PropertyAttributes.None,
typeof(object), new Type[] {});
MethodBuilder getter
typeBuilder.DefineMethod("get_" +
propertyName,
getSetAttr,
typeof(string),
Type.EmptyTypes);
ILGenerator ilGen = getter.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, getFieldBuilder);
ilGen.Emit(OpCodes.Ldstr, propertyName);
ilGen.Emit(OpCodes.Callvirt,
typeof(GetPropertyDelegate).GetMethod("Invoke"));
ilGen.Emit(OpCodes.Ret);
// Define the "set" accessor method for CustomerName.
MethodBuilder setter
typeBuilder.DefineMethod("set_" + propertyName,
getSetAttr,
null,
new Type[] {
typeof(string) });
ilGen = setter.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, setFieldBuilder);
ilGen.Emit(OpCodes.Ldstr, propertyName);
ilGen.Emit(OpCodes.Ldarg_1);
ilGen.Emit(OpCodes.Callvirt,
typeof(SetPropertyDelegate).GetMethod("Invoke"));
ilGen.Emit(OpCodes.Pop);
ilGen.Emit(OpCodes.Ret);
// Last, we must map the two methods created above to
our PropertyBuilder to
// their corresponding behaviors, "get" and
"set"
respectively.
propBuilder.SetGetMethod(getter);
propBuilder.SetSetMethod(setter);
}
return typeBuilder.CreateType();
}
}
}
And here is the ruby code to generate the wrapper type
require ''mscorlib''
require ''C:\Documents and Settings\Josh\My Documents\Visual Studio
2008\Projects\GenerateType\GenerateType\bin\Release\GenerateType.dll''
require ''PresentationFramework, Version=3.0.0.0 <http://3.0.0.0/>
,Culture=neutral, PublicKeyToken=31bf3856ad364e35''
require ''PresentationCore, Version=3.0.0.0 <http://3.0.0.0/> ,
Culture=neutral, PublicKeyToken=31bf3856ad364e35''
require ''WindowsBase, Version=3.0.0.0 <http://3.0.0.0/> ,
Culture=neutral, PublicKeyToken=31bf3856ad364e35''
include System
include System::Collections::Generic
include GenerateType
include System::Collections::ObjectModel
include System::Windows
include System::Windows::Controls
include System::Windows::Data
include System::Windows::Input
class WrapperGenerator
def initialize
@wrapper_cache = {}
end
def wrap(ruby_object)
if ruby_object.is_a? Array
ruby_object.map {|o| wrap(o) }
else
cache(ruby_object) unless cached(ruby_object)
wrapper_class = cached(ruby_object)
wrapper_class.new(ruby_object)
end
end
def invalidate
@wrapper_cache.clear
end
private
def cached(object)
@wrapper_cache[object.class.name <http://object.class.name/> ]
end
def cache(object)
@wrapper_cache[object.class.name <http://object.class.name/> ]
generate_wrapper(object)
end
def generate_wrapper(object)
wrapper_name = "#{object.class.name <http://object.class.name/>
}Wrapper"
properties = List.of(System::String).new
(object.methods - Object.instance_methods).each {|m| properties.add
m.to_s}
wrapper_base_type = TypeGenerator.generate("#{wrapper_name}Base",
properties)
base_instance = Activator.create_instance wrapper_base_type
eval <<EOS
class #{wrapper_name} < base_instance.class
def initialize(original)
self.on_get = lambda do |prop|
original.send prop
end
self.on_set = lambda do |prop, val|
original.send "\#{prop}=", val
end
end
end
return #{wrapper_name} # return the class
EOS
end
end
class Person
attr_accessor :first_name, :last_name
def initialize(first_name, last_name)
@first_name, @last_name = first_name, last_name
end
def full_name
"#{last_name}, #{first_name}"
end
end
# A sample UI
people = []
10.times { |n| people << Person.new("John #{n}",
"Smith") }
wrapper = WrapperGenerator.new
w = Window.new
combo = ComboBox.new
combo.items_source = wrapper.wrap(people) # wrap people objects
combo.display_member_path = "full_name"
combo.margin = 20
w.content = combo
Application.new.run w
-------------- next part --------------
An HTML attachment was scrubbed...
URL:
<http://rubyforge.org/pipermail/ironruby-core/attachments/20081212/cf58e8df/attachment.html>