Bejegyzés

Típus létrehozása futásidőben – RuntimeTypeFactory

“Azok a programozók, akik a régen “szabványos”, úgynevezett procedurális programozáson nevelkedtek egy évtizede megtanulták az objektumorientált programozást. Mert meg kellett.” – graffity 2004-ből.

Aztán, amikor a Java 1.1 beköszöntött és hozta a java.lang.reflect névteret, akkor néztünk nagyokat, hogy mi értelme van egy osztályt felderíteni futásidőben? Pláne, amikor programozás közben ott vannak a property-k és metódusok. Aztán csak-csak hasznát vettük, de a címben említett megoldásra még rémálmunkban sem gondoltunk volna. De a Symbol LAB összefogott és megoldotta ezt is…

1. Keretrendszer

Adott a keretrendszerünk, amiben sok helyen használjuk a reflection-t. Például listáink szűrőablakai nem léteznek önállóan, hanem szűrőosztályokat definiálunk (közös ősből interfésszel). Az osztályok feltérképezésével pedig létre tudjuk hozni a szűrőablakokat. Ha találunk egy property-t, aminek DateInterval a típusa, akkor kikerül két dátumválasztó, amelyek a property-be írnak, onnan olvasnak (DataBinding). A property-k attributumokkal rendelkeznek, amik magyar nevet adnak a mezőknek.
Szép, kidolgozott technológia, meg is számolom… (2 perc eltelik itt) … a Symbol Ügyvitelben jelenleg 103 szűrőobjektum van.

2. Probléma

A feni technológiát kényelmes használni, nem is akarunk letérni az útról, de egyedi fejlesztéseink (SyX) esetén külső fájlból jönnek az osztályok, amelyek nem az 1-es pontban említett ősosztályból származnak. Mi tévők legyünk? Az ügyfél várja a megoldást, ki kell valamit találni…

3. (Egyelőre még nem elégséges) Megoldás

Fejlesztési vezetőnk pénteken már említett valamit, de csak hétfőn állt elő az ötlettel. Ha fel lehet térképezni egy osztályt, akkor miért ne CSINÁLNÁNK egyet, csak úgy futásidőben. Még jó, hogy van Google, mert volt honnan információt meríteni, de hideg zuhanyként ért minket a találatok listája.
Létre lehet hozni típust futásidőben, de a következő lépéseket kell végiggondolni:

  1. Minden property-nek van egy lokális változója
  2. Kell, hogy legyen egy GET és egy SET metódus, amely a property értékét olvassa és írja a lokális változóba/változóból.
  3. A GET és SET metódusok törzse, lényegi része nem C#-ban írandó, hanem a köztes MSIL/IL nyelven, ami a .NET-ben megvalósított Assembly nyelv, azaz szinte gépi kód.

Itt egy kis szomorúság jött, mert ilyet nem oktatnak az egyetemen, nem hétköznapi a felhasználási módja és nincs róla még “MSIL 21 nap alatt” könyv sem.
Viszont a fejlesztési vezetőnk ellentmondást nem tűrve a megoldás útjára lépett és saját maga valósította meg. Ilyenkor kis csendet szokott kérni, de ez most 4 órán át tartott. Megszületett egy már működő megoldás. De ez még nem volt elég jó a felhasználásra.

4. Jó megoldás

További 2 órába telt, hogy megoldjuk a problmát, ugyanis az újonnan létrehozott osztálynak van egy őse, amelynek van egy két paraméteres konstruktora. MSIL nyelven kellett megoldani a base konstruktor hívását és a paraméterek átadását.

5. Összefoglalás

Keretrendszerünk már ezt is tudja, általános osztályt csináltunk belőle, amelyet alant közzéteszünk. Hogy izgalmasabb legyen, egy hibát rejtettünk el benne a 32-33. sor környékén. Aki a hibát kijavítja, +1 pontot kap önéletrajza leadásakor a HR osztálytól. 🙂

using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection.Emit;
using System.Reflection;
namespace SymbolTech.BaseProject.FrameWork.Common
{
    public class RuntimeTypeFactory
    {
        private TypeBuilder tb;
        public RuntimeTypeFactory(string assemblyname, string typename, Type baseclass)
        {
            AssemblyName assname = new AssemblyName(assemblyname);
            AssemblyBuilder assbuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assname, AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder mb = assbuilder.DefineDynamicModule(assname.Name, assname.Name + ".dll");
            tb = mb.DefineType(typename, TypeAttributes.Public, baseclass);
            //Override base constructors
            if (baseclass != null)
                foreach (ConstructorInfo ci in baseclass.GetConstructors())
                {
                    List<Type> types = new List<Type>();
                    foreach (ParameterInfo pari in ci.GetParameters())
                        types.Add(pari.ParameterType);
                    ConstructorBuilder ctor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, types.ToArray());
                    ILGenerator ctorIL = ctor.GetILGenerator();
                    for (byte i = 0; i < types.Count; i++)
                        ctorIL.Emit(OpCodes.Ldarg_0, i);
                    ctorIL.Emit(OpCodes.Call, ci);
                    ctorIL.Emit(OpCodes.Ret);
                }
        }
        public static CustomAttributeBuilder CreateCustomAttributeItem(Type attributetype, Type[] constructorparametertypes, object[] constructorparameters)
        {
            return new CustomAttributeBuilder(attributetype.GetConstructor(constructorparametertypes), constructorparameters);
        }
        public void AddProperty(string name, Type type)
        {
            AddProperty(name, type, null);
        }
        public void AddProperty(string name, Type type, params CustomAttributeBuilder[] customattributes)
        {
            if (String.IsNullOrEmpty(name) || type == null)
                return;
            PropertyBuilder newprop = tb.DefineProperty(name, System.Reflection.PropertyAttributes.HasDefault, type, null);
            if (customattributes != null)
                foreach (CustomAttributeBuilder cab in customattributes)
                    if (cab != null)
                        newprop.SetCustomAttribute(cab);
            FieldBuilder newfield = tb.DefineField(name.ToLower(), type, FieldAttributes.Private);
            MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
            MethodBuilder methodget = tb.DefineMethod(String.Format("get_{0}", name), getSetAttr, type, Type.EmptyTypes);
            ILGenerator methodgetil = methodget.GetILGenerator();
            methodgetil.Emit(OpCodes.Ldarg_0);
            methodgetil.Emit(OpCodes.Ldfld, newfield);
            methodgetil.Emit(OpCodes.Ret);
            MethodBuilder methodset = tb.DefineMethod(String.Format("set_{0}", name), getSetAttr, null, new Type[] { type });
            ILGenerator methodsetil = methodset.GetILGenerator();
            methodsetil.Emit(OpCodes.Ldarg_0);
            methodsetil.Emit(OpCodes.Ldarg_1);
            methodsetil.Emit(OpCodes.Stfld, newfield);
            methodsetil.Emit(OpCodes.Ret);
            newprop.SetGetMethod(methodget);
            newprop.SetSetMethod(methodset);
        }
        public Type CreateType()
        {
            return tb.CreateType();
        }
    }
}

Adatkötés .NET módra – nem eléggé rugalmas

Azt nem kell senkinek ecsetelnem, hogy a .NET-et adatkötés, a DataBinding mennyire nagy szolgálatot tesz, mennyire megkönnyíti az objektumok és felhasználói felületek összekötését, a kapcsolat tipusos kezelését. De mégis belefutottunk tegnap egy hiányosságba és 4 munkaórányi problémamegoldás után el kellett vetnünk tervünket, más megoldást kellett keresni.

Az adatkötésben van egy kis automatizmus, mégpedig az, hogy a rendszer egy vezérlőhöz (UI elem) történő adatkötés során megpróbálja kitalálni, hogy melyik adatkötési mechanizmust akarjuk használni. Egészen pontosan, ha egy TextEdit-hez szeretnénk egy property-t kötni, akkor két dolog történhet.

  1. Amennyiben a forrás objektum egy “sima” objektum, úgy az adatkötés a megnevezett (stringként átadott) property-hez megtörténik. Ha jól paramétereztük fel az adatkötést, akkor a beviteli mező változása a property-ben rögtön megjelenik és vica-versa.
  2. Ha a forrás objektum valamilyen gyűjtemény (IList, IBindingList, Array), akkor a megnevezett property, amihez kötni szeretnénk a gyűjtemény elemeiben kell, hogy létezzen. És az adatkötés is ide történik. List<Customer>-hez való adatkötésnél például a “Name” propertynek a Customer osztályban kell léteznie. Ilyenkor (gyűjtemény esetén) a gyűjtemény elemét vizsgálja a databinding és ott keresi a property-t.

Ez utóbbi teljesen kézenfekvő, hiszen ha egy listát akarunk valahova kötni, akkor a lista egy oszlopát akarjuk valahova kötni. Első esetben egy PropertyManager jön létre, utóbbi esetben pedig egy CurrencyManager. Mindkettőn értelmezett a lépkedés, de az elsőn nincs nagyon értelme.

binding

A mi problémánk viszont a következő. Nem tudjuk befolyásolni, nem tudjuk elérni, hogy egy gyűjtemény típusú forrás objektum adott property-jéhez kössünk vezérlőt. Egy listának is van Count-ja. Ehhez nem tudtunk hozzákötni mondjuk egy Label-t, mert az adatkötés problémaként adta vissza nekünk, hogy a Customer objektumnak nincs Count-ja. De nem is oda akartunk kötni!!!

A probléma megoldása abban rejlik, hogy a property nevét nem szabad kitölteni. Ez már amolyan vicces dolog, hogy egy “változó” megadok a rá való hivatkozással, majd/és opcionálisan még stringként is hozzáírhatom a metódushoz a “változó” nevét.

Jó megoldás: label.DataBindigs.Add("Text", list.Count, "")
Nem jó megoldás: label.DataBindigs.Add("Text", list, "Count")

Hmm, állítólag WPF-ben az adatkötés jobb. Nemsokára úgyis indul egy WPF-es koncepció-projektünk, majd kiderül…