0

Is there any way to change the inlines from a BackgroundWorker?

I tried the following:

private void test()
    {
        var rows = GetDataGridRows(dgVarConfig);
        foreach (DataGridRow r in rows)
        {
            TextBlock tb = cMatchEx.GetCellContent(r) as TextBlock;

            if (!syntaxWorker.IsBusy)
                syntaxWorker.RunWorkerAsync(new KeyValuePair<TextBlock, String>(tb, tb.Text));
        }
    }

    private void syntaxWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        if (e.Argument == null)
            Thread.Sleep(100);
        else
        {
            KeyValuePair<TextBlock, String> kvp = (KeyValuePair<TextBlock, String>)e.Argument;
            e.Result = new KeyValuePair<TextBlock, List<Run>>(kvp.Key, Syntax.Highlight(kvp.Value));
        }
    }


    private void syntaxWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Result != null)
        {
            KeyValuePair<TextBlock, List<Run>> kvp = (KeyValuePair<TextBlock, List<Run>>)e.Result;
            TextBlock tb = kvp.Key;
            tb.Text = "";
            kvp.Value.ForEach(x => tb.Inlines.Add(x));
        }
    }

And the syntax class:

public static class Syntax
{
    static Regex subFormula = new Regex(@"\w+\(\)");
    static Regex sapFormula = new Regex(@"\w+\(([^)]+)\)");
    static Regex strings = new Regex(@"\'[^']+\'");
    static Regex numerals = new Regex(@"\b[0-9\.]+\b");
    static Regex characteristic = new Regex(@"(?:)?\w+(?:)?");
    static Regex andOr = new Regex(@"( and )|( AND )|( or )|( OR )");
    static Regex not = new Regex(@"(not )|(NOT )");

    private static Brush[] colorArray;

    public static List<Run> Highlight(String input)
    {


        colorArray = new Brush[input.Length];

        for (int i = 0; i < input.Length; i++)
            colorArray[i] = Brushes.Black;

        //Reihenfolge beibehalten!!
        assignColor(Brushes.Blue, characteristic.Matches(input));
        assignColor(Brushes.Black, andOr.Matches(input));
        assignColor(Brushes.Black, numerals.Matches(input));
        assignColor(Brushes.Orange, strings.Matches(input));
        assignColor(Brushes.DeepPink, subFormula.Matches(input));
        assignColor(Brushes.Green, sapFormula.Matches(input));
        assignColor(Brushes.Green, not.Matches(input));


        int index = 0;

        List<Run> runList = new List<Run>();

        foreach (Char character in input)
        {
            runList.Add(new Run(character.ToString()) { Foreground = colorArray[index] });
            index++;
        }


        colorArray = null;
        return runList;
    }

    public static void Check(TextBlock textBlock)
    {

    }


    private static void assignColor(Brush brush, MatchCollection matchCollection)
    {
        foreach (Match match in matchCollection)
        {
            int start = match.Index;
            int end = start + match.Length;

            for (int i = start; i < end; i++)
            {
                colorArray[i] = brush;
            }
        }
    }
}

I alway get this error: The calling thread cannot access this object because a different thread owns it.

I tried many different things: return the runList with progress changed, changed the static syntax class to a normal class.. but nothing worked, its always the same error.

I also tried to invoke it from the Backgroundworker.. that means call

    List<Run> runList = Syntax.Highlight(kvp.Value);

this.Dispatcher.Invoke((Action)(() =>
    {
        runList.ForEach(x => publicRunList.Add(x));
    }));

Anybody knows the problem?

Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
Tommehh
  • 872
  • 1
  • 17
  • 44

2 Answers2

1

Use

tb.Dispatcher.Invoke(() => {
    tb.Text = "";
    kvp.Value.ForEach(x => tb.Inlines.Add(x));
});

instead of

tb.Text = "";
kvp.Value.ForEach(x => tb.Inlines.Add(x));

Gui elements can only be accessed from the Gui thread. Using Dispatcher.Invoke ensures that the invoked action runs on it.

You are also creating Run objects in Syntax.Highlight. You also have to create Gui elements on the Gui thread. So you should also wrap this call in a dispatcher invoke:

e.Result = new KeyValuePair<TextBlock, List<Run>>(kvp.Key, Syntax.Highlight(kvp.Value));

This should work:

//this runs synchronously
kvp.Key.Dispatcher.Invoke(() => {
    e.Result = new KeyValuePair<TextBlock, List<Run>>(kvp.Key, Syntax.Highlight(kvp.Value));
});

//this runs asynchronously
kvp.Key.Dispatcher.BeginInvoke((Action)(() => {
    e.Result = new KeyValuePair<TextBlock, List<Run>>(kvp.Key, Syntax.Highlight(kvp.Value));
}));

This probably defeats the purpose of why you wanted to use a BackgroundWorker in the first place. I'd suggest to change the interface of Syntax.Highlight to return a list of tuples with the string and the highlight color instead, and then create the Run objects on the Gui thread.

Edit:

As Gopichandar noted, using BeginInvoke executes the given Action asynchronously, so that would solve the freezing of the application. It would still take a couple of seconds until all elements are added to the Gui though.

Domysee
  • 12,718
  • 10
  • 53
  • 84
  • @ThomasKlammer please check the latest edit, this should fix the problem – Domysee Mar 04 '16 at 07:05
  • so, what do i need a backgroundworker for? Syntax.Highlight does take a while for 2000 entrys in the DataGrid. With this Method my UI Freezes – Tommehh Mar 04 '16 at 07:12
  • @ThomasKlammer as I wrote in the last paragraph, you should change the interface of `Syntax.Highlight` to return a list of tuples that contain the text and the according color. – Domysee Mar 04 '16 at 07:25
  • @ThomasKlammer This way `Syntax.Highlight` can still run in another thread, but you can move the creation of `Run` objects to the UI thread. – Domysee Mar 04 '16 at 07:26
  • i did that, the creation of run objects takes a couple of time so its still freezing :( – Tommehh Mar 04 '16 at 07:27
  • where should i add the "BeginInvoke"? – Tommehh Mar 04 '16 at 07:44
  • @ThomasKlammer have a look at the last code example, I've appended it to illustrate show you where to use BeginInvoke – Domysee Mar 04 '16 at 07:53
  • i get the error message `Cannot convert lambda expression to type 'System.Delegate' because it is not a delegate type` – Tommehh Mar 04 '16 at 07:57
  • there is still a freeze of a couple of seconds.. is there no chance to build up the gui step by step? its not a problem if i see it build up, but the gui should never freeze, not even for 2 seconds – Tommehh Mar 04 '16 at 07:59
  • @ThomasKlammer sure there is, but this is probably enough material for another question. – Domysee Mar 04 '16 at 08:01
  • @ThomasKlammer fixed the syntax error you mentioned – Domysee Mar 04 '16 at 08:05
  • yep, a new thread would be possible. but i can only post every 90 minutes :D – Tommehh Mar 04 '16 at 08:08
  • @ThomasKlammer oh, didn't know there was a limit – Domysee Mar 04 '16 at 08:15
  • @ThomasKlammer is the problem that you get the error `The calling thread cannot access this object because a different thread owns it.` now resolved for you? Or do you need additional information? – Domysee Mar 04 '16 at 08:16
0

In WPF, only the thread that the UI element belongs to (i.e. the UI thread) can communicate with it. The DoWork part of the BackgroundWorker is executed in a different thread and thus cannot do anything UI-related. The same thing goes for Timers instead of BackgroundWorkers.

But if you create the BackgroundWorker with var worker = new BackgroundWorker {WorkerReportsProgress = true}; then you can set an event handler for ProgressChanged. Inside your _DoWork(), you can then say: (sender as BackgroundWorker).ReportProgress, which will call your ProgressChanged event handler in the original thread, where you can manipulate the UI elements.

Full example: http://www.wpf-tutorial.com/misc/multi-threading-with-the-backgroundworker/

Kim Homann
  • 3,042
  • 1
  • 17
  • 20