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>