Results 1 to 5 of 5

Thread: [WPF] Threading - Parsing script in background thread

Threaded View

  1. #1

    Thread Starter
    PowerPoster
    Join Date
    Apr 2007
    Location
    The Netherlands
    Posts
    5,070

    [WPF] Threading - Parsing script in background thread

    Hi,

    I am building a simple script editor application in WPF which parses the text in the script into constructs such as 'classes', 'events', etc (these don't really relate to .NET classes and events, it's a game script).

    Every time the script text changes (eg: the user types into the editor), the script needs to be parsed again so that any possible new classes/events can be picked up. The parsed classes and events are shown in two ComboBoxes above the editor, similar to Visual Studio.

    The problem is that large scripts take a slight amount of time to parse, and this causes the UI to feel sluggish when typing. I am sure I cannot improve the parsing speed any more (and I'm not going to dump the code here and leave you to figure it out), so the obvious approach (at least to me) seems multithreading.

    I want to run the parsing in a background thread, so that the UI does not freeze up for a few milliseconds when the user is typing. The logic seems easy enough; if the user types 1 letter, a background thread is started that starts parsing the script. When that finishes, it sends its results back to the window so it can show the new classes/events. Should the user type again during the time that the background thread is parsing (which will happen if someone is typing at a moderate to fast pace), then the current thread is simply aborted (any results it comes up with are out of date anyway) and a new thread is started.

    I cannot figure out how to do this though, I've never been any good at threading. I keep getting problems where some object tries to access some other object from a different thread, which is of course not possible.

    The most important code is the Script class:
    csharp Code:
    1. public class Script : NotifyObject
    2.     {
    3.         // The container class that holds the thread and the parsing logic
    4.         private ScriptParseThreadContainer _ScriptParseThread;
    5.  
    6.         public Script(string text)
    7.         {
    8.             _ParsedWords = new ObservableCollection<string>();
    9.             this.Text = text;
    10.         }
    11.  
    12.         private string _Text;
    13.         public string Text
    14.         {
    15.             get { return _Text; }
    16.             set
    17.             {
    18.                 _Text = value;
    19.                 this.ParseText();
    20.                 this.OnPropertyChanged(() => this.Text);
    21.             }
    22.         }
    23.  
    24.         private readonly ObservableCollection<string> _ParsedWords;
    25.         public ObservableCollection<string> ParsedWords { get { return _ParsedWords; } }
    26.  
    27.         private void ParseText()
    28.         {
    29.             // Create a new script parse thread container
    30.             if (_ScriptParseThread != null) _ScriptParseThread.Dispose();
    31.             _ScriptParseThread = new ScriptParseThreadContainer(this.Text);
    32.  
    33.             // Hook up event and start it
    34.             _ScriptParseThread.ParsingComplete += ScriptParsingComplete;
    35.             _ScriptParseThread.Start();
    36.         }
    37.  
    38.         private void ScriptParsingComplete(object sender, EventArgs e)
    39.         {
    40.             // When the thread is done parsing, load the resulting words into this ParsedWords collection so the Window can display them
    41.             this.ParsedWords.Clear();
    42.             foreach (var word in _ScriptParseThread.ParsedWords)
    43.             {
    44.                 this.ParsedWords.Add(word);
    45.             }
    46.             _ScriptParseThread.Dispose();
    47.         }
    48.  
    49.         public class ScriptParseThreadContainer : IDisposable
    50.         {
    51.             public ScriptParseThreadContainer(string text)
    52.             {
    53.                 _Thread = new Thread(ParseText);
    54.                 this.Text = text;
    55.                 _ParsedWords = new ObservableCollection<string>();
    56.             }
    57.  
    58.             public event EventHandler ParsingComplete;
    59.  
    60.             public Thread _Thread;
    61.             public string Text { get; set; }
    62.  
    63.             private readonly ObservableCollection<string> _ParsedWords;
    64.             public ObservableCollection<string> ParsedWords { get { return _ParsedWords; } }
    65.  
    66.             public void Start()
    67.             {
    68.                 _Thread.Start();
    69.             }
    70.  
    71.             public void ParseText()
    72.             {
    73.                 this.ParsedWords.Clear();
    74.                 var words = this.Text.Split(' ');
    75.                 foreach (var word in words)
    76.                 {
    77.                     this.ParsedWords.Add(word);
    78.                 }
    79.  
    80.                 if (this.ParsingComplete != null)
    81.                 {
    82.                     this.ParsingComplete(this, EventArgs.Empty);
    83.                 }
    84.             }
    85.  
    86.             public void Dispose()
    87.             {
    88.                 _Thread.Abort();
    89.                 _Thread = null;
    90.             }
    91.         }
    92.     }
    It has properties 'Text' (just the script text) and 'ParsedWords'. For the sake of example I am just extracting the separate words from the script, as the parsing details are not important.

    When the Text property changes, any running parsing thread is aborted and a new one is started. The thread is encapsulated in a class ScriptParseThreadContainer (so that I can pass values such as the Text and retrieve the resulting list of words when it is finished) which extracts all separate words ('parses the text'), puts them in an ObservableCollection<string> (I suppose any collection/array could have done) and then raises the ParsingComplete event.

    The event handler for this ParsingComplete event simply reads the ParsedWords collection and puts each word in its own ParsedWords collection. The MainWindow has a ComboBox that is bound to this property so that it displays that collection.


    This doesn't seem to work. It seems that the event handler for ParsingComplete is still raised on the background thread, so that I am accessing the Script.ParsedWords collection from a different thread that created it resulting in an error.



    How can I do this? It shouldn't be that hard, right? I just need to run a thread that puts some strings into a collection, then I need it to notify me when it is finished, at which point I put those words in my own collection where the Window can read them...

    Any help? Thanks!


    EDIT
    I might have gotten a bit further, not there yet though...

    I suppose I have to somehow put the words from one collection to the other in a method which should then be called from the UI thread. Similar to how accessing controls from background threads works in Winforms? In that case I create a delegate and invoke that via the Control.Invoke method. I don't have any control here to call Invoke on though, so I found the next best thing: Dispatcher.CurrentDispatcher?
    csharp Code:
    1. private void ScriptParsingComplete(object sender, EventArgs e)
    2.         {
    3.             // When the thread is done parsing, load the resulting words into this ParsedWords collection so the Window can display them
    4.             var del = new LoadWordsDelegate(LoadWords);
    5.             Dispatcher.CurrentDispatcher.BeginInvoke(del);
    6.             _ScriptParseThread.Dispose();
    7.         }
    8.  
    9.         private delegate void LoadWordsDelegate();
    10.         private void LoadWords()
    11.         {
    12.             this.ParsedWords.Clear();
    13.             foreach (var word in _ScriptParseThread.ParsedWords)
    14.             {
    15.                 this.ParsedWords.Add(word);
    16.             }
    17.         }
    This seems more appropriate, but still doesn't work. When using Dispatcher.CurrentDispatcher.BeginInvoke, the LoadWords method is never called. I can now type into the textbox without errors, but the ComboBox is never filled and stays empty. When I replace that line by Dispatcher.CurrentDispatcher.Invoke, the method is called, but still on the background thread it seems... It results in the same error as if I don't use the delegate invoking at all.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width