Xamarin.AndroidでISerializableする話

結論=>無意味。JSONあたりにシリアライズして文字列としてやり取りしよう。

以下結論に至るまでことの顛末を記す。
Androidな開発でクラスをシリアライズすることは多々ある。Activityの遷移とか保存とかでBundkeにシリアライズして受け渡したりとか。そこでクラスを作る。Javaのシリアライザを使いたいのでJava.Lang.Objectから派生しよう。

class JavaSerializable : Java.Lang.Object, Java.IO.ISerializable { }

ところがこれだと復元時に例外が発生し、復元されない。

E/AndroidRuntime(15922): FATAL EXCEPTION: main
E/AndroidRuntime(15922): Process: App1.App1, PID: 15922
E/AndroidRuntime(15922): android.runtime.JavaProxyThrowable: System.NotSupportedException: Unable to activate instance of type App1.Moc from native handle 0x1d (key_handle 0x2bc79163). ---> System.MissingMethodException: No constructor found for App1.Moc::.ctor(System.IntPtr, Android.Runtime.JniHandleOwnership) ---> Java.Interop.JavaLocationException: Exception of type 'Java.Interop.JavaLocationException' was thrown.

原因はXamarinがインスタンスの復旧処理を行う場合(IntPtr, Android.Runtime.JniHandleOwnership)なコンストラクタを介して行うからだそうだ。つまり下記で回避できる。

class JavaSerializable : Java.Lang.Object, Java.IO.ISerializable {
    public JavaSerializable() { }
    public JavaSerializable(
        IntPtr handle,
        Android.Runtime.JniHandleOwnership transfer
    ) : base(handle, transfer) { }
}


しかしこれでエラーは出なくなるもののよくよく見るとメンバが規定値で復元されていない。調べてみるとJavaのISerializableはコンパイラがreadObject, writeObjectメソッドを生成して処理するけどXamarinはそんなことしてくれないので自分で作って保存復元処理を書く必要がある。それを踏まえてついでにコードの再利用を鑑みると下の感じに。

class JavaSerializable : Java.Lang.Object, Java.IO.ISerializable {
    public JavaSerializable() { }
    public JavaSerializable(
        IntPtr handle,
        Android.Runtime.JniHandleOwnership transfer
    ) : base(handle, transfer) { }

    [Export("readObject", Throws = new[] {
        typeof (Java.IO.IOException),
        typeof (Java.Lang.ClassNotFoundException)})]
    private void __ReadObject(Java.IO.ObjectInputStream source) {
        var byteArray = new byte[source.Available()];
        source.Read(byteArray);
        using (var memStream = new MemoryStream()) {
            var binForm = new BinaryFormatter();
            memStream.Write(byteArray, 0, byteArray.Length);
            memStream.Seek(0, SeekOrigin.Begin);
            var obj = binForm.Deserialize(memStream);
            this.Obj = obj;
        }
    }

    [Export("writeObject", Throws = new[] {
        typeof (Java.IO.IOException),
        typeof (Java.Lang.ClassNotFoundException)})]
    private void __WriteObject(Java.IO.ObjectOutputStream destination) {
        var bf = new BinaryFormatter();
        using (var ms = new MemoryStream()) {
            bf.Serialize(ms, this.Obj);

            destination.Write(ms.ToArray());
        }
    }

    public object Obj { get; set; }
}


C#だと*thisの差し替えなんてことはできないので折衷案としてプロパティObjに保存したいクラスを格納して~といった感じになるなった。しかしObjがobject型なのはキモい。ジェネリックを使ってタイプセーフにしたいところである。が、ISerializableなクラスをジェネリッククラスにすると復元時にジェネリックパラメータが不明なので復元できない。困った。

さて途中でお気づきだと思うが自分でbyteにシリアライズ、デシリアライズするならクラス化の必要なくbyteでBndleにputすればよい気がするし、今時ならJSONシリアライズしてやりとりしたほうが絶対再利用性が高い。

という感じで結論に至る。

Xamarin Formsのマウスイベントが少ない話の続き

yarukizero.hatenablog.jp

前回書かなかったレンダラの実装。今回は実際に書いたコードを張り付けるだけの簡単なお仕事なので前回の話と微妙に違う。

まず共通部分。

public interface IClickableView {
	bool CallClick();
	bool CallPointerDown();
	bool CallPointerUp();
}

public class ClickableView : global::Xamarin.Forms.BoxView, IClickableView {
	public static readonly BindableProperty IsPointerDownProperty =
		BindableProperty.Create(nameof(IsPointerDown), typeof(bool), typeof(ClickableView), default(bool),
			propertyChanged: (bindable, oldValue, newValue) => ((ClickableView)bindable).IsPointerDown = (bool)newValue);

	public bool IsPointerDown {
		get { return (bool)GetValue(IsPointerDownProperty); }
		set { SetValue(IsPointerDownProperty, value); }
	}
	
	public static readonly BindableProperty ForegroundColorProperty =
		BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(ClickableView), default(Color),
			propertyChanged: (bindable, oldValue, newValue) =>	((ClickableView)bindable).ForegroundColor = (Color)newValue);

	public Color ForegroundColor {
		get { return (Color)GetValue(ForegroundColorProperty); }
		set { SetValue(ForegroundColorProperty, value); }
	}
	
	public event EventHandler<EventArgs> Clicked;
	public event EventHandler<EventArgs> PointerDown;
	public event EventHandler<EventArgs> PointerUp;

	bool IClickableView.CallClick() {
		Clicked?.Invoke(this, EventArgs.Empty);

		return Clicked != null;
	}

	bool IClickableView.CallPointerDown() {
		IsPointerDown = true;
		PointerDown?.Invoke(this, EventArgs.Empty);

		return PointerDown != null;
	}
	bool IClickableView.CallPointerUp() {
		IsPointerDown = false;
		PointerUp?.Invoke(this, EventArgs.Empty);

		return PointerUp != null;
	}
}

プラットフォーム個別実装でandroid

public class ClickableViewHelper {
    public class Helper<T> where T : Android.Views.View, IVisualElementRenderer {
        private IClickableView target;

        public Helper(T renderer) {
            renderer.ElementChanged += (s, e) =>
            {
                if (e.NewElement == null) {
                    // 後片付け必要?
                    return;
                } else { 
                    target = e.NewElement as IClickableView;
                    System.Diagnostics.Debug.Assert(target != null);
                    renderer.Touch += (snder, ev) =>
                    {
                        var pointerIndex = ((int)ev.Event.Action) >> ((int)MotionEventActions.PointerIndexShift) & ((int)MotionEventActions.PointerIndexMask);
                        switch (ev.Event.Action & MotionEventActions.Mask) {
                            case MotionEventActions.Down:
                                ev.Handled = target.CallPointerDown();
                                break;
                            case MotionEventActions.Up:
                                ev.Handled = target.CallPointerUp();
                                if(new Rectangle(
                                    0, 0,
                                    renderer.Width, renderer.Height).Contains(
                                        ev.Event.GetX(), ev.Event.GetY())) {

                                    target.CallClick();
                                }
                                break;

                            case MotionEventActions.Cancel:
                                break;

                        }
                    };
                }
            };
        }
    }

    public static Helper<T> Create<T>(T renderer) where T : Android.Views.View, IVisualElementRenderer {
        return new Helper<T>(renderer);
    }
}

public class ClickableViewRenderer : BoxRenderer {
    private object helper;
    public ClickableViewRenderer() {
        helper = ClickableViewHelper.Create(this);
    }
}

UWPはこちら。

public class ClickableViewHelper {
    public class Helper<T> where T : Windows.UI.Xaml.Controls.Panel, IVisualElementRenderer {
        private IClickableView target;

        public Helper(T renderer) {
            renderer.ElementChanged += (s, e) =>
            {
                if (e.NewElement == null) {
                    // 後片付け必要?

                    return;
                } else {
                    target = e.NewElement as IClickableView;
                    System.Diagnostics.Debug.Assert(target != null);

                    renderer.PointerPressed += (sender, ev) =>
                    {
                        renderer.CapturePointer(ev.Pointer);
                        target?.CallPointerDown();
                    };
                    renderer.PointerReleased += (sender, ev) =>
                    {
                        renderer.ReleasePointerCapture(ev.Pointer);
                        if (target != null) {
                            target.CallPointerUp();

                            var pt = ev.GetCurrentPoint(renderer);
                            if ((0 <= pt.Position.X)
                                && (pt.Position.X <= renderer.ActualWidth)
                                && (0 <= pt.Position.Y)
                                && (pt.Position.Y <= renderer.ActualHeight)) {

                                target.CallClick();
                            }
                        }
                    };
                }
            };
        }
    }

    public static Helper<T> Create<T>(T renderer) where T : Windows.UI.Xaml.Controls.Panel, IVisualElementRenderer {
        return new Helper<T>(renderer);
    }
}

public class ClickableViewRenderer : BoxViewRenderer {
    private object helper;
    public ClickableViewRenderer() {
        helper = ClickableViewHelper.Create(this);
    }
}

Xamarin Formsのマウスイベントが少ない話

この文章はローカルで書きためておいたので現状すでに乖離している可能性があります。

ここ最近xamarinを使用しているのだけどプラットフォーム互換が障害になっているのかマウス/タッチイベント系がなくて困る。MouseDown/Upとかそういうの。ないものはないので仕方がないわけで自作しようとかそんな話。

ということでBoxViewを継承して下記みたいなViewを作る。UWPをサポートする都合上Pointerという一番抽象度が高いかなとか思うUWPの名前にした。対応するレンダラはそのまま素直に作ればよいので特に問題は出ないと思う。PointerDown/Upイベントがないとかは用意したけど今回使っていないし、なくても困らない気もするので書いていない。

 public class ClickablieView : BoxView {
     public static readonly BindableProperty IsPointerDownProperty;
     public static readonly BindableProperty ForegroundColorProperty;

     public bool IsPointerDown { get; set; }
     public Color ForegroundColor { get; set; }

     public event EventHandler<EventArgs> Clicked;
 }

できたところでXAMLをかく。作ったClickableViewを下に、上にLabelを載せることでボタンぽい見た目としている。定義したForegroundColorはそのまま使用するのではなく、ボタンのテキスト色としてバインドする用途に使う。

 <!-- ボタンのコンテナ -->
 <Grid WidthRequest="128" HeightRequest="48">
  <c:ClickableView x:Name="button" ForegroundColor="White" >
    <View.Triggers>
      <Trigger TargetType="c:ClickableView" Property="IsPointerDown" Value="True">
        <Setter Property="BackgroundColor" Value="Black" />
        <Setter Property="ForegroundColor" Value="Red" />
      </Trigger>
    </View.Triggers>
    <View.Behaviors>
      <i:EventToCommand 
          EventName="Clicked"
          Command="{Binding ButtonClickCommand}" />
    </View.Behaviors>
  </c:ClickableView>
  <Label
    Style="{StaticResource ButtonTextStyle}" 
    Text="新規" 
    TextColor="{Binding Source={x:Reference button},Path=ForegroundColor}" />
 </Grid>

で、androidはこれで特に問題がないのだけどUWPはLabelがマウス処理イベントを奪うという問題に直面する。なんでそんなことになっているのか(UWPのデフォがそうなのか、xamarinがなにかやっているのか)調べてないけど、困るので対策を入れる。下記、

 public class AzLabelRenderer : LabelRenderer {
     private Reactive.Bindings.ReactiveProperty<bool> t;
     
     protected override void OnElementChanged(ElementChangedEventArgs<Label> e) {
         base.OnElementChanged(e);
         
         t = new Reactive.Bindings.ReactiveProperty<bool>();
         t.PropertyChanged += (s, ev) =>
         {
             if (t.Value) {
                 t.Value = false;
             }
         };
         
         data.BindingOperations.SetBinding(this,
             Windows.UI.Xaml.Controls.TextBlock.IsHitTestVisibleProperty,
             new data.Binding()
                 {
                     Path = new Windows.UI.Xaml.PropertyPath("Value"),
                     Source = t,
                     Mode = data.BindingMode.TwoWay,
                 });
         this.IsHitTestVisible = false;
     }
 }

IsHitTestが常にfalseとなるように細工する。特にReactivePropertyを使う必要はないのだけど、よそで使っているのにこれだけのためにクラス作るのもめどいので利用する。ビバ怠惰。

長くなったのでおわる。