0

I'm having issues updating the UI threads. Application is running 1 UI thread for each form, meaning just using SyncronizationContext with the UI thread doesn't work. I'm doing this for looping update performance as well as modal popup possibilities like select value before you can use the form.

How I'm creating it in ApplicationContext:

public AppContext()
    {
        
    foreach(var form in activeForms)
            {
                
                form.Load += Form_Load;
                form.FormClosed += Form_FormClosed;
                StartFormInSeparateThread(form);
                //form.Show();
            }
}
private void StartFormInSeparateThread(Form form)
    {
        Thread thread = new Thread(() =>
        {
            
            Application.Run(form);
        });
        thread.ApartmentState = ApartmentState.STA;
        thread.Start();
        
    }

There are controls on each for that are databound and updating with values from the same databound object. Controls being Labels and DataGridview (bound to a bindinglist). What would be ideal is having the Bindinglist threadsafe and execute on these multiple UI threads. Found some examples that I attempted like this:

List<SynchronizationContext> listctx = new();

public ThreadSafeBindingList2()
{
    //SynchronizationContext ctx = SynchronizationContext.Current;
    //listctx.Add(ctx);
}
public void SyncContxt()
{
    SynchronizationContext ctx = SynchronizationContext.Current;
    listctx.Add(ctx);
}
protected override void OnAddingNew(AddingNewEventArgs e)
{
    for (int i = 0; i < listctx.Count; i++)
    {
        if (listctx[i] == null)
        {
            BaseAddingNew(e);
        }
        else
        {
            listctx[i].Send(delegate
            {
                BaseAddingNew(e);
            }, null);
        }
    }
}
void BaseAddingNew(AddingNewEventArgs e)
{ 
    base.OnAddingNew(e); 
}

protected override void OnListChanged(ListChangedEventArgs e)
{
    for (int i = 0; i < listctx.Count; i++)
    {
        if (listctx[i] == null)
        {
            BaseListChanged(e);
        }
        else
        {
            listctx[i].Send(delegate
            {
                
                BaseListChanged(e);
            }, null);
        }
    }
}

void BaseListChanged(ListChangedEventArgs e)
{
    base.OnListChanged(e); 
} 

I'm also using a static class as a data property change hub for all controls so I don't change the databinding source more than once (again due to performance), where I have a background worker "ticking" every 1-3 seconds depending on system load:

 private static void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e)
    {
        if (timerStart is false)
        {
            Thread.Sleep(6000);
            timerStart = true;
        }
        while (DisplayTimerUpdateBGW.CancellationPending is false)
        {
            
            //UIThread.Post((object stat) => //Send
            //{
            threadSleepTimer = OrderList.Where(x => x.Status != OrderOrderlineStatus.Claimed).ToList().Count > 20 ? 2000 : 1000;
            if (OrderList.Count > 40)
                threadSleepTimer = 3000;

            UpdateDisplayTimer();

            //}, null);

            Thread.Sleep(threadSleepTimer);
        }
    } 
 private static void UpdateDisplayTimer()
    {
        var displayLoopStartTimer = DateTime.Now;
        TimeSpan displayLoopEndTimer = new();

        Span<int> orderID = CollectionsMarshal.AsSpan(OrderList.Select(x => x.ID).ToList());
        for (int i = 0; i < orderID.Length; i++)
        {
            OrderModel order = OrderList[i];
            order.OrderInfo = "Ble";
            Span<int> OrderLineID = CollectionsMarshal.AsSpan(order.Orderlines.Select(x => x.Id).ToList());
            for (int j = 0; j < OrderLineID.Length; j++)
            {
                OrderlineModel ol = order.Orderlines[j];
                TimeSpan TotalElapsedTime = ol.OrderlineCompletedTimeStamp != null ? (TimeSpan)(ol.OrderlineCompletedTimeStamp - ol.OrderlineReceivedTimeStamp) : DateTime.Now - ol.OrderlineReceivedTimeStamp;
                string displaytimerValue = "";

                if (ol.OrderlineCompletedTimeStamp == null)
                    displaytimerValue = TotalElapsedTime.ToString(@"mm\:ss");
                else
                    displaytimerValue = $" {(DateTime.Now - ol.OrderlineCompletedTimeStamp)?.ToString(@"mm\:ss")} \n({TotalElapsedTime.ToString(@"mm\:ss")})";

                ol.DisplayTimer = displaytimerValue;

            }
        }
    }

Ideally I want to have the labels and datagridview properties databindings so that I can have INotifyPropertyChanged just updating these relevant properties in all UI threads.

Any help would be appreciated!

Ifirit
  • 41
  • 6
  • There are numerous questions and answers around multithreading and winforms on here. You will have to use Invoke when a UI element is on a different thread. just look at https://stackoverflow.com/questions/142003/cross-thread-operation-not-valid-control-accessed-from-a-thread-other-than-the or even https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread – CobyC Dec 14 '22 at 12:55
  • @JonasH As stated in the question it's for modal and performance reasons. When running it all on a single UI thread using syncronizationContext it worked "fine" however when getting 80+ usercontrols inside the FlowLayoutpanel the performance tanked creating a poor experience where the user had the UI thread locked 1/3 of the time. The modal "select an employee before continuing" as well is a "nice to have" thing that shouldn't lock the other users of the system from their screens. – Ifirit Dec 14 '22 at 13:00
  • 1
    In most cases performance problems can be addressed in other ways. I find it very likely that you are doing a bunch of unnecessary work, like updating the UI far to often. You should start by doing some profiling to find the actual problem. And having multiple users using the same process seem *very* odd. Users should not only have their own process, they should have their own windows session. – JonasH Dec 14 '22 at 13:06
  • Thanks @CobyC, yes I read the post you sent, and I understand that you invoke on another thread to send it to a UI element. I'm more or less just looking for a collection/list that is overridden to use something like ISyncronisationInvoke to target the UI threads. – Ifirit Dec 14 '22 at 13:07
  • @JonasH It is a bit odd yes. The UC has orderlines that has a ticking timer every 1,2 or 3 seconds given the time the loop is taking. e.i one Usercontrol with x amount of orderlines in a datagridview (+ some lables buttons). There are two forms that you can have multiple instances of as well. As we know the datagridviews are not the best when updating their information and hence I'm just trying to optimize everything else while not blocking the UI thread more than necessary. I'm leaning to go back to SyncronizationContext and 1 UI thread instead, but thought I'd give this a go first. – Ifirit Dec 14 '22 at 13:16
  • Clear case of https://www.youtube.com/watch?v=V9crbuWUIzE – Fildor Dec 14 '22 at 14:27

1 Answers1

2

One of many ways to look at this is that there's only one display area (albeit which might consist of many screens) and only one element of it can change at any given moment. To my way of thinking, this means that having more than one UI thread can often be self defeating (unless your UI is testing another UI). And since the machine has some finite number of cores, having a very large number of threads (whether of the UI or worker variety) means you can start to have a lot of overhead marshalling the context as threads switch off.

If we wanted to make a Minimal Reproducible Example that has 10 Form objects executing continuous "mock update" tasks in parallel, what we could do instead of the "data property change hub" you mentioned is to implement INotifyPropertyChanged in those form classes with static PropertyChanged event that gets fired when the update occurs. To mock data binding where FormWithLongRunningTask is the binding source, the main form subscribes to the PropertyChanged event and adds a new Record to the BindingList<Record> by identifying the sender and inspecting e to determine which property has changed. In this case, if the property is TimeStamp, the received data is marshalled onto the one-and-only UI thread to display the result in the DataGridView.

public partial class MainForm : Form
{
    public MainForm() => InitializeComponent();
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        // Subscribe to the static event here.
        FormWithLongRunningTask.PropertyChanged += onAnyFWLRTPropertyChanged;
        // Start up the 10 forms which will do "popcorn" updates.
        for (int i = 0; i < 10; i++)
        {
            new FormWithLongRunningTask { Name = $"Form{i}" }.Show(this);
        }
    }
    private void onAnyFWLRTPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (sender is FormWithLongRunningTask form)
        {
            BeginInvoke(() =>
            {
                switch (e.PropertyName)
                {
                    case nameof(FormWithLongRunningTask.TimeStamp):
                        dataGridViewEx.DataSource.Add(new Record
                        {
                            Sender = form.Name,
                            TimeStamp = form.TimeStamp,
                        });
                        break;
                    default:
                        break;
                }
            });
        }
    }
}

main form with DataGridView


The DataGridView on the main form uses this custom class:

class DataGridViewEx : DataGridView
{
    public new BindingList<Record> DataSource { get; } = new BindingList<Record>();
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        if (!DesignMode)
        {
            base.DataSource = this.DataSource;
            AllowUserToAddRows = false;

            #region F O R M A T    C O L U M N S
            DataSource.Add(new Record());
            Columns[nameof(Record.Sender)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            var col = Columns[nameof(Record.TimeStamp)];
            col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
            col.DefaultCellStyle.Format = "hh:mm:ss tt";
            DataSource.Clear();
            #endregion F O R M A T    C O L U M N S
        }
    }
    protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
    {
        base.OnCellPainting(e);
        if ((e.RowIndex > -1) && (e.RowIndex < DataSource.Count))
        {
            var record = DataSource[e.RowIndex];
            var color = _colors[int.Parse(record.Sender.Replace("Form", string.Empty))];
            e.CellStyle.ForeColor = color;
            if (e.ColumnIndex > 0)
            {
                CurrentCell = this[0, e.RowIndex];
            }
        }
    }
    Color[] _colors = new Color[]
    {
        Color.Black, Color.Blue, Color.Green, Color.LightSalmon, Color.SeaGreen,
        Color.BlueViolet, Color.DarkCyan, Color.Maroon, Color.Chocolate, Color.DarkKhaki
    };
}    
class Record
{
    public string Sender { get; set; } = string.Empty;
    public DateTime TimeStamp { get; set; }
}

The 'other' 10 forms use this class which mocks a binding source like this:

public partial class FormWithLongRunningTask : Form, INotifyPropertyChanged
{
    static Random _rando = new Random(8);
    public FormWithLongRunningTask() => InitializeComponent();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        _ = runRandomDelayLoop();
    }
    private async Task runRandomDelayLoop()
    {
        while(true)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(_rando.NextDouble() * 10));
                TimeStamp = DateTime.Now;
                Text = $"@ {TimeStamp.ToLongTimeString()}";
                BringToFront();
            }
            catch (ObjectDisposedException)
            {
            }
        }
    }
    DateTime _timeStamp = DateTime.Now;
    public DateTime TimeStamp
    {
        get => _timeStamp;
        set
        {
            if (!Equals(_timeStamp, value))
            {
                _timeStamp = value;
                OnPropertyChanged();
            }
        }
    }
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
    {
        add => PropertyChanged += value;
        remove => PropertyChanged -= value;
    }
    public static event PropertyChangedEventHandler? PropertyChanged;
}

I believe that there's no 'right' answer to your question but I hope there's something here that might move things forward for you.

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • [Clone](https://github.com/IVSoftware/ui-form-multithreading.git) this sample. – IVSoftware Dec 14 '22 at 16:18
  • Thanks for the detailed example @IVSoftware! I'll try adopt this type of thinking to the problem and see if it solves it. If I'm understanding this correctly I'll have to have each property that I want to change on the baseUCs that are added in the form. Adding the INotifyPropertyChanged then is no issue, but demands a restructuring of the base components and model structures. I really appreciate this help though, and I'll experiment to see what I can come up with. – Ifirit Dec 15 '22 at 07:39
  • I believe you _are_ understanding correctly. I'll mention that this kind of "restructuring" might pay off in general, because under the hood when we bind this or that usually something like `INotifyPropertyChange` or `INotifyCollectionChanged` is running things. Is `MVVM` something you're familiar with? When the data "model" is decoupled _entirely_ from the UI it's portable when you go to port your app on to Android. The UI responds to property changes, and also drives the model with actions (e.g. based on `ICommand` interface. There's a lot here, though because you're asking great questions! – IVSoftware Dec 15 '22 at 13:45
  • I also noted your comments mentioning "select employee before continuing" and "blocking the UI 1/3 of the time". What it _seems_ you're trying to do might be more performant if you were to consider adopting an `async` model and `await` actions that you post to background `Task`. Also consider using `BeginInvoke`. A different [answer](https://stackoverflow.com/a/74736871/5438626) of mine comes to mind because that OP wanted to have a "login screen before continuing" with the main window. I definitely agree with you that we don't want to block the UI thread for more than an instant at a time. – IVSoftware Dec 15 '22 at 13:59
  • I don't know why I didn't use a `DataGridView` for the example in the first place (slaps forehead...). The code has been edited - I just think it might be more tailored to your post that way. – IVSoftware Dec 15 '22 at 15:06
  • Thank you! I really appreciate this. Yes I'm primarily using MVVM(S) when using Xamarin/MAUI. I'm not the best at overriding controls, but it seems like the datagridview you made are in line with what I'm looking for (nice touch with the column formating). Select Employee would be a modal `public static DialogResult ShowDialogWindow() { using (EmployeeForm form = new()) { return form .ShowDialog(); } }` Demanding a selection event before proceeding to log who does what. – Ifirit Dec 16 '22 at 08:50
  • All the "actions" where running in a backgroundWorker before, but I understand running all the work in a separate Task to not clog up the UI thread. I will most likely adopt the tasks to preform the heavy lifting when needed. The goal I believe is to not block the UI thread more than 0.05s at the time to not make it sluggish. I'll make a side-project to test the architectural changes and performance. Again really appreciate this! – Ifirit Dec 16 '22 at 09:06