16

I have a visual element MyButton with a custom renderer implemented for iOS.

Shared:

namespace RendererTest
{
    public class MyButton: Button
    {
        public Color BoundaryColor { get; set; }
    }

    public static class App
    {
        public static Page GetMainPage()
        {    
            var button = new MyButton { Text = "Click me!", BoundaryColor = Color.Red };
            button.Clicked += (sender, e) => (sender as MyButton).BoundaryColor = Color.Blue;
            return new ContentPage { Content = button };
        }
    }
}

iOS:

[assembly:ExportRenderer(typeof(MyButton), typeof(MyButtonRenderer))]

namespace RendererTest.iOS
{
    public class MyButtonRenderer: ButtonRenderer
    {
        public override void Draw(RectangleF rect)
        {
            using (var context = UIGraphics.GetCurrentContext()) {
                context.SetFillColor(Element.BackgroundColor.ToCGColor());
                context.SetStrokeColor((Element as MyButton).BoundaryColor.ToCGColor());
                context.SetLineWidth(10);
                context.AddPath(CGPath.FromRect(Bounds));
                context.DrawPath(CGPathDrawingMode.FillStroke);
            }
        }
    }
}

When pressing the button, the red boundary should become blue. Apparently the renderer does not notice the changed property. How can I trigger a redraw?

(This example is for iOS. But my question applies to Android as well.)

Falko
  • 17,076
  • 13
  • 60
  • 105

2 Answers2

10

First, turn you BoundaryColor into a bindable property. That's not required, firing INPC event is enough, but then you can bind to it:

public static readonly BindableProperty BoundaryColorProperty =
    BindableProperty.Create ("BoundaryColor", typeof(Color), typeof(MyButton), Color.Default);

public Color BoundaryColor {
    get { return (Color)GetValue (BoudaryColorProperty); }
    set { SetValue (BoundaryColorProperty, value); }
}

then, in your renderer:

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

    if (e.PropertyName == MyButton.BoundaryColorProperty.PropertyName)
        SetNeedsDisplay ();
}
Stephane Delcroix
  • 16,134
  • 5
  • 57
  • 85
  • I tried to fire a ```public event Action BoundaryColorChanged``` when setting the ```BoundaryColor```. Within the ```MyButtonRenderer``` constructor I would subscribe to that event and call ```SetNeedsDisplay()```. If doing so, however, I get a ```System.Reflection.TargetInvocationException```. – Falko Aug 05 '14 at 13:54
  • It is working if I don't subscribe in the renderer's constructor, but within the ```Draw``` method. But this is probably not the best solution, since I'll keep adding (```+=```) listeners to the event. – Falko Aug 05 '14 at 17:51
  • you don't have to attach to the PropertyChanged event, `OnElementChanged` is already attached for you, you just have to override it. – Stephane Delcroix Aug 06 '14 at 07:11
  • @StephaneDelcroix what is the equivalent of SetNeedsDisplay (); in Android? because I need to do it on Android too. – Tomasz Kowalczyk Aug 23 '14 at 08:46
10

Two modifications were required:

  1. Call OnPropertyChanged within the setter of the BoundaryColor property:

    public class MyButton: Button
    {
        Color boundaryColor = Color.Red;
    
        public Color BoundaryColor {
            get {
                return boundaryColor;
            }
            set {
                boundaryColor = value;
                OnPropertyChanged();  // <-- here
            }
        }
    }
    
  2. Subscribe to the event within the OnElementChanged method of MyButtonRenderer:

    public class MyButtonRenderer: ButtonRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);
            Element.PropertyChanged += (s_, e_) => SetNeedsDisplay();  // <-- here
        }
    
        public override void Draw(RectangleF rect)
        {
            // ...
        }
    }
    

Note: It seems to be important to subscribe within OnElementChanged and not the constructor. Otherwise a System.Reflection.TargetInvocationException is raised.

Falko
  • 17,076
  • 13
  • 60
  • 105