Xamarin.Forms 用の超軽量プレビューアを作る | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/8669
で、XAMLにクリックイベントが入っていると XamlLoader がパースエラーになるので、そのイベントをうまい具合に無視しなければいけないのですが、じゃあ、もともとある ContentPage クラスに後からイベントを追加できたらうまくスルーできるのではないか?と思って、継承可能な DynamicObject を探していました。
正確に言えば、DynamicObject は継承可能なので、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 を作っておいて、後追いで次のようにプロパティを増やすことが可能です。
1 2 3 | 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 なメソッドを生やすことができるんじゃないだろうか、と考えて、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 インターフェースを付けて、内部的に再実装しようという試みです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 を使っておくと、
1 2 3 4 5 6 7 8 9 10 11 12 | 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 をパースするときに対応するメソッドを探してしまうらしいんですね。
1 2 3 4 | System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> 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 例外を発生させている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 ; } |