0

I am trying to create a view representing a vertical line that moves horizontally within another view.

I have the following ViewController and custom view.

using System;
using System.Timers;
using AppKit;
using CoreGraphics;

namespace CustomControlExperiment
{
    public partial class ViewController : NSViewController
    {
        private Timer _timer;
        private LineControl _lineControl;

        public ViewController(IntPtr handle) : base(handle)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            var frame = new CGRect { X = 0, Y = 0, Height = ContentView.Frame.Height, Width = 1 };
            _lineControl = new LineControl(frame);

            ContentView.AddSubview(_lineControl);

            _timer = new Timer(500);
            _timer.Elapsed += _timer_Elapsed;
            _timer.Start();

        }

        private void _timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            _lineControl.Position++;
        }
    }

    public class LineControl : NSView
    {
        private float _position;

        public LineControl(CGRect frameRect) : base(frameRect)
        {
            Initialize();
        }

        private void Initialize()
        {
            WantsLayer = true;
        }

        public override void DrawRect(CGRect dirtyRect)
        {
            Console.WriteLine("DrawRect");
            base.DrawRect(dirtyRect);

            var context = NSGraphicsContext.CurrentContext?.CGContext;

            if (context == null)
            {
                return;
            }

            var height = Frame.Height;

            context.SetStrokeColor(new CGColor(.5f, .6f, .7f));
            context.SetLineWidth(1f);
            context.SetLineDash(0, new nfloat[] { 3, 3 });

            context.MoveTo(0, 0);
            context.AddLineToPoint(0, height);

            context.StrokePath();
        }

        public float Position
        {
            get => _position;
            set
            {
                _position = value;
                Console.WriteLine("Position = " + _position);

                NeedsDisplay = true;
            }
        }
    }
}

DrawRect is called once when the program is first run. After that it is not called again, despite Position being incremented and NeedsDisplay being set.

What am I doing wrong?

fractor
  • 1,534
  • 2
  • 15
  • 30
  • 1
    I don't know if it's the cause of your issue, but your `System.Timers` `timer` elapsed event is run on a thread pool thread. It's not legal to set `needsDisplay` outside the main thread. If you try to do that in Xcode, you will get a runtime error from the thread sanitizer. You need to update `Position` from the main thread, or marshal the call to set `needsDisplay` onto the main thread. – TheNextman Jun 20 '19 at 04:19
  • This did cross my mind but hadn't investigated it yet as in other situations I had an exception. So it fails silently. Anyway, great stuff, thanks. Please post as an answer :) – fractor Jun 20 '19 at 07:10

1 Answers1

1

The System.Timers.Timer elapsed event is run on a thread pool thread - it's not legal to set needsDisplay outside the main thread. If you try this in Xcode, it's a runtime error from the thread sanitizer.

You need to modify your code so that Position is updated from the main thread, or marshal the call to set needsDisplay onto the main thread.

There are various approaches to that in .NET and Xamarin - here is one document suggesting to use InvokeOnMainThread (it's for iOS but I believe it applies to macOS also).

TheNextman
  • 12,428
  • 2
  • 36
  • 75