Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

10000's+ UI elements, bind or draw?

I am drawing a header for a timeline control. It looks like this: enter image description here

I go to 0.01 millisecond per line, so for a 10 minute timeline I am looking at drawing 60000 lines + 6000 labels. This takes a while, ~10 seconds. I would like to offload this from the UI thread. My code is currently:

private void drawHeader()
{
  Header.Children.Clear();
  switch (viewLevel)
  {
    case ViewLevel.MilliSeconds100:
        double hWidth = Header.Width;
        this.drawHeaderLines(new TimeSpan(0, 0, 0, 0, 10), 100, 5, hWidth);

        //Was looking into background worker to off load UI

        //backgroundWorker = new BackgroundWorker();

        //backgroundWorker.DoWork += delegate(object sender, DoWorkEventArgs args)
        //                               {
        //                                   this.drawHeaderLines(new TimeSpan(0, 0, 0, 0, 10), 100, 5, hWidth);
        //                               };
        //backgroundWorker.RunWorkerAsync();
        break;
    }
}

private void drawHeaderLines(TimeSpan timeStep, int majorEveryXLine, int distanceBetweenLines, double headerWidth)
{
var currentTime = new TimeSpan(0, 0, 0, 0, 0);
const int everyXLine100 = 10;
double currentX = 0;
var currentLine = 0;
while (currentX < headerWidth)
{
    var l = new Line
                {
                    ToolTip = currentTime.ToString(@"hh\:mm\:ss\.fff"),
                    StrokeThickness = 1,
                    X1 = 0,
                    X2 = 0,
                    Y1 = 30,
                    Y2 = 25
                };
    if (((currentLine % majorEveryXLine) == 0) && currentLine != 0)
    {
        l.StrokeThickness = 2;
        l.Y2 = 15;
        var textBlock = new TextBlock
                            {
                                Text = l.ToolTip.ToString(),
                                FontSize = 8,
                                FontFamily = new FontFamily("Tahoma"),
                                Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255))
                            };

        Canvas.SetLeft(textBlock, (currentX - 22));
        Canvas.SetTop(textBlock, 0);
        Header.Children.Add(textBlock);
    }

    if ((((currentLine % everyXLine100) == 0) && currentLine != 0)
        && (currentLine % majorEveryXLine) != 0)
    {
        l.Y2 = 20;
        var textBlock = new TextBlock
                            {
                                Text = string.Format(".{0}", TimeSpan.Parse(l.ToolTip.ToString()).Milliseconds),
                                                            FontSize = 8,
                                                            FontFamily = new FontFamily("Tahoma"),
                                                            Foreground = new SolidColorBrush(Color.FromRgb(192, 192, 192))
                            };

        Canvas.SetLeft(textBlock, (currentX - 8));
        Canvas.SetTop(textBlock, 8);
        Header.Children.Add(textBlock);
    }
    l.Stroke = new SolidColorBrush(Color.FromRgb(255, 255, 255));
    Header.Children.Add(l);
    Canvas.SetLeft(l, currentX);

    currentX += distanceBetweenLines;
    currentLine++;
    currentTime += timeStep;
}
}

I had looked into BackgroundWorker, except you can't create UI elements on a non-UI thread.

Is it possible at all to do drawHeaderLines in a non-UI thread?

Could I use data binding for drawing the lines? Would this help with UI responsiveness?

I would imagine I can use databinding, but the Styling is probably beyond my current WPF ability (coming from winforms and trying to learn what all these style objects are and binding them).

Would anyone be able to supply a starting point for tempting this out? Or Google a tutorial that would get me started?

like image 586
jpiccolo Avatar asked Nov 03 '22 11:11

jpiccolo


1 Answers

So, as you have said, all of this work needs to be done in the UI thread; you can't just do it in a background thread.

However, running a very long loop doing a lot of UI modifications in the UI thread blocks the UI thread, so clearly we can't do that.

They key here is that you need to break up what you're doing into many smaller units of work, and then do all of those small units of work in the UI thread. The UI thread is doing just as much work as before (possibly even a tad more, due to the overhead of managing all of this) but it allows other things (such as mouse move/click events, key presses, etc.) to happen in-between those tasks.

Here is a simple example:

private void button1_Click(object sender, EventArgs e)
{
    TaskScheduler uiContext = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Run(async () =>
    {
        for (int i = 0; i < 1000; i++)
        {
            await Task.Factory.StartNew(() =>
            {
                Controls.Add(new Label() { Text = i.ToString() });
            }, CancellationToken.None, TaskCreationOptions.None, uiContext);
        }
    });
}

First we grab the UI context while in the UI thread, then we start a new thread in the background that will be responsible for starting up all of our little tasks. Here you would have the start of your loop. (You may want to exract this out to another method, since yours isn't as simple.) Then, immediately inside of the loop start a new task, and have that task started in the UI's context. Inside of that task you can then place the entire body of what was in your loop. By awaiting on that task you ensure that each one is scheduled as a continuation of the previous, so they all run in order. If the order doesn't matter (which is unlikely, but possible) then you don't need to await at all.

If you need a C# 4.0 version of this you could keep one Task out of the loop, and in each iteration wire up a new task as a continuation of the previous, and then set "itself" as that task. It would be messier though.

like image 120
Servy Avatar answered Nov 11 '22 06:11

Servy