継承可能なDynamicObjectを作ろうとしたが挫折中

Xamarin.Forms 用の超軽量プレビューアを作る | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/8669

で、XAMLにクリックイベントが入っていると XamlLoader がパースエラーになるので、そのイベントをうまい具合に無視しなければいけないのですが、じゃあ、もともとある ContentPage クラスに後からイベントを追加できたらうまくスルーできるのではないか?と思って、継承可能な DynamicObject を探していました。

正確に言えば、DynamicObject は継承可能なので、

public class DynamicViewModel : DynamicObject
{
    Dictionary<string, object> dic = new Dictionary<string, object>();

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return dic.TryGetValue(binder.Name, out result);
    }
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        dic[binder.Name] = value;
        return true;
    }
}

な感じで DynamicObject を継承した ViewModel を作っておいて、後追いで次のようにプロパティを増やすことが可能です。

dynamic vm = new DynamicViewModel();
vm.Title = "Hello";
var title = vm.Title;

dynamic なので、インテリセンスは効かないけど、うまくくるめば XML や JSON をマッピングすることができます。ちなみに、Newtonsoft.Json.Linq.JObject を使うと、WPF の ViewModel としてそのまま使えます。何故か、Xamarin.Forms では使えないので、呼び出し方が微妙に違うのかなと。呼び出せるほうが不思議な感じがするのですが。.NET Framework と Profile259 の Runtime の違いかもしれません。

DynamicViewModel な方法は、ASP.NET の ViewBag にも使われているので割とポピュラーな手段です。詳細は、

メタプログラミング.NET | Kevin Hazzard, Jason Bock
https://www.amazon.co.jp/dp/4048867741

な本にも書いてあります。随分前だけど、

MVVMパターンでViewModelを楽に作る方法 – かずきのBlog@hatena
http://blog.okazuki.jp/entry/20100702/1278056325

なところで、MSDN マガジンへのリンクもあります。と言う訳で、じゃあ、DynamicObject を継承して ContentPage に後付けで Clicked なメソッドを生やすことができるんじゃないだろうか、と考えて、

public class SubPage : ContentPage, DynamicObject
{
    public SubPage() { }

    /*
    private void Button_Clicked(object sender, EventArgs e)
    {

    }
    */
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        // 読み捨て
        System.Diagnostics.Debug.WriteLine("called: " + binder.Name);
        result = null;
        return true;
    }
}

なことを考えたのですが、ダメです。C# は多重継承ができないから、ContentPage と DynamicObject の両方を基底に持つことはできないんですね。じゃあ、どっちかをインターフェースにして、内部で再実装させればいいと思ったわけで、となると DynamicObject のほうをインターフェースにしたいですよね。ってことであれこれ探すとそれっぽいものがありました。

remi/MetaObject: Simple dynamic method invocation for your .NET objects
https://github.com/remi/MetaObject

IDynamicMetaObjectProvider インターフェースを付けて、内部的に再実装しようという試みです。

public class DynamicContnetPage : ContentPage, IDynamicMetaObjectProvider
{
    #region MetaObject
    public DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression e)
    {
        return new MetaObject(e, this);
    }
    #endregion

    Dictionary<string, object>; dic = new Dictionary<string, object>();

    public virtual System.Collections.Generic.IEnumerable<string> GetDynamicMemberNames()
    {
        // return Value.GetDynamicMemberNames();
        return new string[] { };
    }

    public virtual bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        result = null;
        return true;
    }
    public virtual bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return dic.TryGetValue(binder.Name, out result);
    }
    public virtual bool TrySetMember(SetMemberBinder binder, object value)
    {
        dic[binder.Name] = value;
        return true;
    }
}

こんな風に MetaObject を使っておくと、

public class SubPage : DynamicContnetPage
{
    public SubPage() { }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        // 読み捨て
        System.Diagnostics.Debug.WriteLine("called: " + binder.Name);
        result = null;
        return true;
    }
}

こんな風に、Clicked イベントやらを読み捨ててくれるはずです。中身で何か実装すれば、デバッグログとか通信っぽいものもできますね。このあたりは、実 DynamicObject のコードを見るといいのですが、中身的に IDynamicMetaObjectProvider インターフェースが DynamicMetaObject GetMetaObject(Expression parameter) を要求するのでメタデータを用意しておかないという仕組み&制限なのです。でもって、これが「式 Expression」を要求するというメタ構造になっていて、えらい大変なことになってます。

さて、これで万事解決と思いきや、いざコンパイルしてみると、MetaObject のコードがビルドできません。なんと、MetaObject は .NET Framework 専用なんですね。ああ、Xamarin.Forms の PCL は Profile259 なので .NET Runtime を使う訳なので、微妙に異なる訳です。仕方がないので、Runtime のほうに書き直そうかとしたら案の定 System.Reflection の中身が違うので、GetMethod を GetRuntimeMethod に直したりしながら、ええ、GetConstructor がないので、GetTypeInfo().DeclaredConstructors に変えてみたりと、あれこれとビルドが通るように修正。

で、なんとかビルドが通ったものを Xamarin.Forms の PCL に配置していざ、XamlLoader を動かすと、嗚呼、TryInvokeMember が呼び出される前に例外をはいて落ちてしまいます。どうやら、XAML に Clicked イベントを書くと XAML をパースするときに対応するメソッドを探してしまうらしいんですね。

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---&gt; Xamarin.Forms.Xaml.XamlParseException: Position 13:37. No method Button_Clicked found on type XamlPreview.SubPage
  at Xamarin.Forms.Xaml.ApplyPropertiesVisitor.SetPropertyValue (System.Object xamlelement, Xamarin.Forms.Xaml.XmlName propertyName, System.Object value, System.Object rootElement, Xamarin.Forms.Xaml.INode node, Xamarin.Forms.Xaml.HydratationContext context, System.Xml.IXmlLineInfo lineInfo) [0x000de] in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ApplyPropertiesVisitor.cs:310 
  at Xamarin.Forms.Xaml.ApplyPropertiesVisitor.Visit (Xamarin.Forms.Xaml.ValueNode node, Xamarin.Forms.Xaml.INode parentNode) [0x00070] in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ApplyPropertiesVisitor.cs:63 
...

ここで、どんな方法で探しているのかが不明(たぶんリフレクション?)なので、ちょっとこの先は解らず。仕方がないから Xamarin.Forms のコードを読むか、別な対策を立てるか思案中。

 

あった、

in ApplyPropertiesVisitor.cs で XamlParseException 例外を発生させている。

		static bool TryConnectEvent(object element, string localName, object value, object rootElement, IXmlLineInfo lineInfo, out Exception exception)
		{
			exception = null;

			var elementType = element.GetType();
			var eventInfo = elementType.GetRuntimeEvent(localName);
			var stringValue = value as string;

			if (eventInfo == null || IsNullOrEmpty(stringValue))
				return false;

			var methodInfo = rootElement.GetType().GetRuntimeMethods().FirstOrDefault(mi => mi.Name == (string)value);
			if (methodInfo == null) {
				exception = new XamlParseException($"No method {value} found on type {rootElement.GetType()}", lineInfo);
				return false;
			}

			try {
				eventInfo.AddEventHandler(element, methodInfo.CreateDelegate(eventInfo.EventHandlerType, rootElement));
				return true;
			} catch (ArgumentException ae) {
				exception = new XamlParseException($"Method {stringValue} does not have the correct signature", lineInfo, ae);
			}
			return false;
		}
カテゴリー: 開発, XAML, Xamarin パーマリンク