[Ironruby-core] WPF databinding with ruby objects. Workaround :)

Joshua Nussbaum joshnuss at gmail.com
Fri Dec 12 01:55:55 EST 2008


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>


More information about the Ironruby-core mailing list