Using Dispatcher correctly in event to update UI

2019-08-10 22:46发布

I'm using dispatchers to update a bound collection from an event. I just ran into a nasty issue where I had two different dispatchers in the same event and it wasn't working. Using the debugger it was completely skipping over the code in the first dispatcher. Putting the entire event in a single dispatcher fixed it. I assume it's because of how the compiler handles it, can anyone confirm this - only one dispatcher per event, at least when dealing with the same elements?

Here is the code, when it gets to the await after (line == 0), it exits the function completely. Later, when line !=0 it runs the "Old style menu" fine. If I put all of the code in a single dispatcher, everything works fine.

    private async void ProcessNLS(string parameters) // NET/USB List Info
    {
        if (parameters.Substring(0, 1) == "A" || (parameters.Substring(0, 1) == "U")) // ASCII x08/2010  Only
        {
            int line = Convert.ToInt32(parameters.Substring(1, 1));
            string text = parameters.Substring(3);

            // New Menu, Clear Old - Use Last Received/Holding Menu: See NLT bug
            if (line == 0)
            {
                await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    State.Menu.ServiceType = State.holdingMenu.ServiceType;
        ...
                    State.Menu.Items.Clear();
                });

                OnMenuTitleInfoChanged(new MenuTitleInfoChangedArgs(State.Menu));

                // Replace Network Top with custom menu
                if (State.Menu.LayerInfo == LayerTypes.NetworkTop)
                {
                    await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                    {
                        State.Menu.Items.Clear();
                    });

        ...
                }

                // Get 1st Advanced Menu
                if (Device.SupportsAdvancedMenus & State.Menu.LayerInfo != LayerTypes.NetworkTop)
                {
        ...
                }

            }

            // Old style menu
            if (!Device.SupportsAdvancedMenus && State.Menu.LayerInfo != LayerTypes.NetworkTop)
            {
                NetworkMenuItem menuItem = new NetworkMenuItem(line, text);

                await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    State.Menu.Items.Add(menuItem);
                });

                OnMenuLoading(new MenuLoadingArgs(menuItem));
            }
        }

        // C - Track Cursor
        if (parameters.Substring(0,1) == "C")
        {
            if (parameters.Substring(1, 1)== "-")
            {
                // No Cursor
                // Sent when entering player screen
                await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
        ...
                    State.Menu.Items.Clear();

                    OnMenuTitleInfoChanged(new MenuTitleInfoChangedArgs(State.Menu));
                }
            }

        });
    }

Like this it would just jump over the dispatcher for no apparent reason. If I put the entire thing in a single dispatcher it works fine.

A second question, if I have another event with a dispatcher, something like this:

        foreach (xxx)
        {
            if (xxx == yyy)
            {
                await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
                {
                    State.Menu.Items.Add(menuItem);
                });
            }
        }

Would it be preferable to instead wrap the entire foreach loop in a dispatcher rather then calling it when needed each iteration?

Since my original question has changed I've made a new post with more specifics and another possible solution by just wrapping the original socket listener task in a dispatcher

Possible solution to issue with multiple UI dispatchers in the same method?

*** Update:

I think Raymond is on the right track, though adding Task didn't fix it, I noticed although it starts processing line "0" of the menu, before it sets up the new menu it tries to process the next line "1" command which is ignored because it doesn't have the right menu state yet, it still hasn't been set by the previous command yet.

I'm not sure how to fix it, it seems like I have to do an await at a lower level so be sure sure it full finishes one command before starting the next (and not sure why putting the whole ProcessNLS in UI dispatcher works), it's a little complicated since I go through multiple levels but here is the flow:

        socket = new StreamSocket();
        try
        {
            await socket.ConnectAsync(new HostName(HostName), Port);
            OnConnect(new EventArgs());
            await Task.Factory.StartNew(WaitForMessage);
        }
        catch (Exception ex)
        {
            OnConnectionFail(new EventArgs());
        }

Goes to:

private async void WaitForMessage() 
{
...

                foreach (var message in messages)
                {
                    if (string.IsNullOrWhiteSpace(message))
                        continue;

                    ProcessMessage(message);
                }

}

Goes to

    private void ProcessMessage(string message, string optionalFlags = "")
    {
...
            case "NLS": // NET/USB List Info
                ProcessNLS(parameters);
                break;       
    }

to finally

 private async void ProcessNLS(string parameters) // NET/USB List Info

My alternate solution is to put to ProcessMessage call under WaitForMessage in a UI dispatcher

*** Update #2

I think this may be working, here is the updated flow, have to await multiple steps, use task instead of void

    private async void WaitForMessage()
    {
 ...
                foreach (var message in messages)
                {
                    if (string.IsNullOrWhiteSpace(message))
                        continue;

                    await ProcessMessage(message);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine("WaitForMessage Error: " + ex.Message);
            OnDisconnect(new EventArgs());
        }
    }

to

    private async Task ProcessMessage(string message, string optionalFlags = "")
    {
        ...
           case "NLS": // NET/USB List Info
                await ProcessNLS(parameters);
                break;

        }

to

    private async Task ProcessNLS(string parameters) // NET/USB List Info

1条回答
淡お忘
2楼-- · 2019-08-10 23:06

The problem is here:

private async void ProcessNLS(...)
        ^^^^^^^^^^

You declared an async void function, which means "When the first await happens, return from the function immediately, and let the rest of the work run asynchronously." If you want the caller to be able to await on completion of your function, change the signature to private async Task ProcessNLS(...).

查看更多
登录 后发表回答