简体   繁体   中英

How to set Xamarin.Forms Elements BindableProperties from a Custom Renderer?

I have been trying to set a bindable property value in my Element from my native control through a custom renderer. My native control is a view (painview) where you can draw and I am trying to get the drawing and set it, as a base64 string, to a bindable property Signature in my Element.

This is my Native Control

public class PaintView : View
{
    Canvas _drawCanvas;
    Bitmap _canvasBitmap;
    readonly Paint _paint;
    readonly Dictionary<int, MotionEvent.PointerCoords> _coords = new Dictionary<int, MotionEvent.PointerCoords>();
    
    public Bitmap CanvasBitmap { get => _canvasBitmap; private set => _canvasBitmap = value; }

    private readonly string TAG = nameof(PaintView);

    public event EventHandler OnLineDrawn;

    public PaintView(Context context) : base(context, null, 0)
    {
        _paint = new Paint() { Color = Color.Blue, StrokeWidth = 5f, AntiAlias = true };
        _paint.SetStyle(Paint.Style.Stroke);
    }
    public PaintView(Context context, IAttributeSet attrs) : base(context, attrs) { }
    public PaintView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { }

    protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
    {
        base.OnSizeChanged(w, h, oldw, oldh);
        _canvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); // full-screen bitmap
        _drawCanvas = new Canvas(_canvasBitmap); // the canvas will draw into the bitmap
    }

    public override bool OnTouchEvent(MotionEvent e)
    {
        switch (e.ActionMasked)
        {
            case MotionEventActions.Down:
                {
                    int id = e.GetPointerId(0);
                    var start = new MotionEvent.PointerCoords();
                    e.GetPointerCoords(id, start);
                    _coords.Add(id, start);
                    return true;
                }
            case MotionEventActions.PointerDown:
                {
                    int id = e.GetPointerId(e.ActionIndex);
                    var start = new MotionEvent.PointerCoords();
                    e.GetPointerCoords(id, start);
                    _coords.Add(id, start);
                    return true;
                }
            case MotionEventActions.Move:
                {
                    for (int index = 0; index < e.PointerCount; index++)
                    {
                        var id = e.GetPointerId(index);
                        float x = e.GetX(index);
                        float y = e.GetY(index);
                        _drawCanvas.DrawLine(_coords[id].X, _coords[id].Y, x, y, _paint);
                        _coords[id].X = x;
                        _coords[id].Y = y;
                        OnLineDrawn?.Invoke(this, EventArgs.Empty);
                    }

                    Invalidate();
                    return true;
                }
            case MotionEventActions.PointerUp:
                {
                    int id = e.GetPointerId(e.ActionIndex);
                    _coords.Remove(id);
                    return true;
                }
            case MotionEventActions.Up:
                {
                    int id = e.GetPointerId(0);
                    _coords.Remove(id);
                    return true;
                }
            default:
                return false;
        }
    }

    protected override void OnDraw(Canvas canvas)
    {
        // Copy the off-screen canvas data onto the View from it's associated Bitmap (which stores the actual drawn data)
        canvas.DrawBitmap(_canvasBitmap, 0, 0, null);
    }

    public void Clear()
    {
        _drawCanvas.DrawColor(Color.Black, PorterDuff.Mode.Clear); // Paint the off-screen buffer black
        Invalidate(); // Call Invalidate to redraw the view
    }

    public void SetInkColor(Color color)
    {
        _paint.Color = color;
    }
}

The property PaintView._canvasBitmap is the one I want to be set in my Xamarin.Form Element through my custom renderer.

This is my Custom Renderer

public class SketchViewRenderer : ViewRenderer<SketchView, PaintView>
{
    public SketchViewRenderer(Context context) : base(context) { }

    protected override void OnElementChanged(ElementChangedEventArgs<SketchView> e)
    {
        if (Control == null)
        {
            var paintView = new PaintView(Context);
            paintView.SetInkColor(Element.InkColor.ToAndroid());
            SetNativeControl(new PaintView(Context));
            MessagingCenter.Subscribe<SketchView>(this, nameof(SketchView.OnClear), OnMessageClear);
            Control.OnLineDrawn += PaintViewLineDrawn;
        }
    }

    private void PaintViewLineDrawn(object sender, EventArgs e)
    {
        var sketchCrl = (ISketchViewController)Element;

        if (sketchCrl == null) return;

        try
        {
            Element.SetValueFromRenderer(SketchView.SignatureProperty, Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
            sketchCrl.SendSketchUpdated(Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (e.PropertyName == SketchView.InkColorProperty.PropertyName)
        {
            Control.SetInkColor(Element.InkColor.ToAndroid());
        }
        if (e.PropertyName == SketchView.ClearProperty.PropertyName)
        {
            if (Element.Clear) OnMessageClear(Element);
        }
    }

    private void OnMessageClear(SketchView sender)
    {
        if (sender == Element) Control.Clear();            
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            MessagingCenter.Unsubscribe<SketchView>(this, nameof(SketchView.OnClear));
            Control.OnLineDrawn -= PaintViewLineDrawn;
        }

        base.Dispose(disposing);
    }
}

I have tried changing my Element.Signature property through the SketchViewRenderer.PaintViewLineDrawn(...) method without success. This has been prove when debugging my view model where the property has not been set as expected.

My Xamarin.Forms Element looks as follow

public class SketchView : View, IDoubleTappedController, ISketchViewController
{
    public static readonly BindableProperty SignatureProperty = BindableProperty.Create(nameof(Signature), typeof(string), typeof(SketchView), null, defaultBindingMode: BindingMode.TwoWay);
    public string Signature
    {
        get => (string)GetValue(SignatureProperty);
        set => SetValue(SignatureProperty, value);
    }

    public static readonly BindableProperty MultiTouchEnabledProperty = BindableProperty.Create(nameof(MultiTouchEnabled), typeof(bool), typeof(SketchView), false);
    public bool MultiTouchEnabled
    {
        get => (bool)GetValue(MultiTouchEnabledProperty);
        set => SetValue(MultiTouchEnabledProperty, value);
    }

    public static readonly BindableProperty InkColorProperty = BindableProperty.Create(nameof(InkColor), typeof(Xamarin.Forms.Color), typeof(SketchView), Xamarin.Forms.Color.Azure);
    public Xamarin.Forms.Color InkColor
    {
        get => (Xamarin.Forms.Color)GetValue(InkColorProperty);
        set => SetValue(InkColorProperty, value);
    }

    public static readonly BindableProperty ClearProperty = BindableProperty.Create(nameof(Clear), typeof(bool), typeof(SketchView), false, defaultBindingMode: BindingMode.TwoWay);
    public bool Clear
    {
        get => (bool)GetValue(ClearProperty);
        set
        {
            SetValue(ClearProperty, value);

            if (value) { OnClear(); }
        }
    }

    public void OnClear()
    {
        MessagingCenter.Send(this, nameof(OnClear));
    }

    public void SetSignature(string signature)
    {
        Signature = signature;
    }

    void IDoubleTappedController.DoubleTapped()
    {
        throw new NotImplementedException();
    }

    void ISketchViewController.SendSketchUpdated(string signature)
    {
        Clear = false;
        Signature = signature;
    }
}

I have also tried using the SetValueFromRenderer() method from my Custom renderer, again, without success.

May you suggest to me what is the way to set an Element value from a Custom Renderer?

Thanks and kind regards,

Temo

The problem was that the field in my view model was set to null when comparing it with the value. Then throwing a TargetException letting the source buggy unable to be updated by the target.

public bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = default)
    {
        if (value == null) return false;
        if (field != null && field.Equals(value)) return false;

        field = value;

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        return true;
    }

Now, I make sure the field is not null before using the Equals operator.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM