Change TextBlock.Inlines from Backgroundworker

252 views Asked by At

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?

2

There are 2 answers

14
Domysee On

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.

0
Kim Homann On

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/