首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何使用CommunityToolkit.Mvvm.Messaging和WinUI 3在线程之间发送消息?

如何使用CommunityToolkit.Mvvm.Messaging和WinUI 3在线程之间发送消息?
EN

Stack Overflow用户
提问于 2022-11-05 16:01:19
回答 1查看 107关注 0票数 0

下面这个简单的多线程程序旨在尝试文档中所述的CommunityToolkit Messenger包(参见:信使)。

WeakReferenceMessengerStrongReferenceMessenger都公开了一个Default属性,该属性在包中提供了一个线程安全实现。

我曾希望这将意味着我可以在一个线程上发送消息,并在其他线程上接收它们,但是似乎IMessenger Interface出现了一个问题。详情如下。

这个项目从一个普通的TemplateStudio WinUI 3 (v1.1.5)桌面模板开始,它使用CommunityToolkit Mvvm包(带有Messenger)和一个页面MainPage。当App启动时,它启动一个RandomMessageGenerator线程,该线程使用来自ToolkitWeakReferenceMessenger.Default通道周期性地发出TraceMessage。UI线程接收这些消息并将它们存储在List中。

App.xaml.cs

代码语言:javascript
运行
复制
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;

using Multi_Window.Activation;
using Multi_Window.Contracts.Services;
using Multi_Window.Core.Contracts.Services;
using Multi_Window.Core.Services;
using Multi_Window.Services;
using Multi_Window.ViewModels;
using Multi_Window.Views;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using CommunityToolkit.Mvvm.Messaging.Messages;
using System.Diagnostics;

namespace Multi_Window;

// To learn more about WinUI 3, see https://docs.microsoft.com/windows/apps/winui/winui3/.
public partial class App : Application
{
    // The .NET Generic Host provides dependency injection, configuration, logging, and other services.
    // https://docs.microsoft.com/dotnet/core/extensions/generic-host
    // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection
    // https://docs.microsoft.com/dotnet/core/extensions/configuration
    // https://docs.microsoft.com/dotnet/core/extensions/logging
    public IHost Host { get; }

    public static T GetService<T>()
        where T : class
    {
        if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
        {
            throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
        }

        return service;
    }

    public static WindowEx MainWindow { get; } = new MainWindow();
    public static ShellPage? ShellPage  { get; set; }
    private static readonly List<string> _traceMessages = new();

    private Task? messageGenerator;

    public App()
    {
        InitializeComponent();

        Host = Microsoft.Extensions.Hosting.Host.
        CreateDefaultBuilder().
        UseContentRoot(AppContext.BaseDirectory).
        ConfigureServices((context, services) =>
        {
            // Default Activation Handler
            services.AddTransient<ActivationHandler<LaunchActivatedEventArgs>, DefaultActivationHandler>();

            // Other Activation Handlers

            // Services
            services.AddTransient<INavigationViewService, NavigationViewService>();

            services.AddSingleton<IActivationService, ActivationService>();
            services.AddSingleton<IPageService, PageService>();
            services.AddSingleton<INavigationService, NavigationService>();

            // Core Services
            services.AddSingleton<IFileService, FileService>();

            // Views and ViewModels
            services.AddTransient<MainViewModel>();
            services.AddTransient<MainPage>();

            // ** NOTE ** changed to Singleton so we can refer to THE ShellPage/ShellViewModel
            services.AddSingleton<ShellPage>();
            services.AddSingleton<ShellViewModel>();

            // Configuration
        }).
        Build();

        UnhandledException += App_UnhandledException;
        System.AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
        Microsoft.UI.Xaml.Application.Current.UnhandledException += Current_UnhandledException;
    }

    private void RandomMessageGenerator()
    {
        var shutdown = false;
        WeakReferenceMessenger.Default.Register<ShutDownMessage>(this, (r, m) => shutdown = true);
        Debug.WriteLine($"RandomMessageGenerator started on thread {Environment.CurrentManagedThreadId}");

        Random rnd = new();
        // not a good way to control thread shutdown in general but will do for a quick test
        while (shutdown == false)
        {
            Thread.Sleep(rnd.Next(5000));
            var tm = new TraceMessage($"{DateTime.Now:hh:mm:ss.ffff} Timer event. (Th: {Environment.CurrentManagedThreadId})");
            try
            {
                WeakReferenceMessenger.Default.Send(tm);
            }
            catch (Exception e) 
            {
                Debug.WriteLine(e.Message);
                break;
            }
        }
        Debug.WriteLine($"RandomMessageGenerator closed at {DateTime.Now:hh:mm:ss.ffff} (Th: {Environment.CurrentManagedThreadId})");
    }

    private void Current_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) => throw new NotImplementedException();
    private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e) => throw new NotImplementedException();

    private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
    {
        // TODO: Log and handle exceptions as appropriate.
        // https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception.
        throw new NotImplementedException();
    }

    protected async override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);

        await App.GetService<IActivationService>().ActivateAsync(args);

        MainWindow.AppWindow.Closing += OnAppWindowClosing;

        WeakReferenceMessenger.Default.Register<TraceMessage>(this, (r, m) =>
        {
            _traceMessages.Add(m.Value);
            Debug.WriteLine(m.Value);
        });
        WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) => OnStatusWindowClosed());           // StatusWindow closed events
        WeakReferenceMessenger.Default.Register<App, TraceMessagesRequest>(this, (r, m) => m.Reply(_traceMessages));    // StatusWindow requests previous messages

        messageGenerator = Task.Run(RandomMessageGenerator);
    }

    private void OnStatusWindowClosed()
    {
        if (ShellPage is not null && ShellPage.SettingsStatusWindow)
        {
            ShellPage.SettingsStatusWindow = false;                                                     // turn off toggle
            if (ShellPage.NavigationFrame.Content is MainPage settingsPage) settingsPage.StatusWindowToggle.IsOn = false;
        }
    }

    private async void OnAppWindowClosing(object sender, AppWindowClosingEventArgs e)
    {
        WeakReferenceMessenger.Default.UnregisterAll(this);                                             // stop messages and avoid memory leaks
        WeakReferenceMessenger.Default.Send(new ShutDownMessage(true));                                 // close all windows

        MainWindow.AppWindow.Closing -= OnAppWindowClosing;

        if (messageGenerator is not null) await messageGenerator;
    }
}

用户可以通过在StatusWindow上切换一个开关来创建一个Window ( UI线程上的辅助Window)。StatusWindow应该从App中打开、请求和加载以前的消息,然后注册新的TraceMessages。所有TraceMessages (包括新的)都显示在StatusWindow上的一个ListView中。

MainPage.xaml.cs

代码语言:javascript
运行
复制
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

using Multi_Window.ViewModels;

namespace Multi_Window.Views;

public sealed partial class MainPage : Page
{
    public MainViewModel ViewModel { get; } = App.GetService<MainViewModel>();
    public ShellPage ShellPage { get; } = App.GetService<ShellPage>();
    public ShellViewModel ShellViewModel { get; } = App.GetService<ShellViewModel>();

    public MainPage()
    {
        DataContext = ViewModel;
        InitializeComponent();
    }

    private void StatusWindow_Toggled(object sender, RoutedEventArgs e)
    {
        if (StatusWindowToggle.IsOn && ShellPage.SettingsStatusWindow == false)
        {
            StatusWindow window = new() { Title = "Prosper Status" };
            window.Activate();
            ShellPage.SettingsStatusWindow = true;
        }
        else if (StatusWindowToggle.IsOn == false && ShellPage.SettingsStatusWindow == true)
            WeakReferenceMessenger.Default.Send(new CloseWindowMessage(true));
    }
}

MainPage.xaml

代码语言:javascript
运行
复制
<Page
    x:Class="Multi_Window.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid x:Name="ContentArea">
        <ToggleSwitch x:Name="StatusWindowToggle" x:FieldModifier="public" Grid.Row="2" Grid.Column="1" Header="Show Status Window"
                          Toggled="StatusWindow_Toggled" IsOn="{x:Bind ShellPage.SettingsStatusWindow, Mode=OneTime}" />
    </Grid>
</Page>

StatusWindow.xaml.cs

代码语言:javascript
运行
复制
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace Multi_Window.Views;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class StatusWindow : Window
{
    private ObservableCollection<string> _traceMessages { get; } = new();

    public StatusWindow()
    {
        InitializeComponent();
        var sw = new WindowPrimitives(this);
        sw.AppWindow.SetIcon("Assets/wip.ico");

        WeakReferenceMessenger.Default.Register<CloseWindowMessage>(this, (r, m) => Close());
        WeakReferenceMessenger.Default.Register<ShutDownMessage>(this, (r, m) => Close());
    }

    private void StatusWindow_Closed(object sender, WindowEventArgs args)
    {
        WeakReferenceMessenger.Default.UnregisterAll(this);                                             // stop getting messages and avoid memory leaks
        WeakReferenceMessenger.Default.Send(new WindowClosedMessage(true));                             // acknowledge closure
    }

    private void StatusMessages_Loaded(object sender, RoutedEventArgs e)
    {
        // get current Trace messages
        var messages = WeakReferenceMessenger.Default.Send<TraceMessagesRequest>();
        if (messages != null && messages.Responses.Count > 0)
            foreach (var response in messages.Responses)
                foreach (var trace in response)
                    _traceMessages.Add(trace);

        // register for Trace messages and, when they arrive, add them to list
        WeakReferenceMessenger.Default.Register<TraceMessage>(this, (r, m) => _traceMessages.Add(m.Value));
    }
}

StatusPage.xaml

代码语言:javascript
运行
复制
<Window
    x:Class="Multi_Window.Views.StatusWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Closed="StatusWindow_Closed"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListView x:Name="StatusMessages" x:FieldModifier="public" VerticalAlignment="Top" Margin="20" SelectionMode="None" BorderBrush="Black" BorderThickness="1"
                    ItemsSource="{x:Bind _traceMessages, Mode=OneWay}"
                    ScrollViewer.HorizontalScrollMode="Enabled"
                    ScrollViewer.HorizontalScrollBarVisibility="Visible"
                    ScrollViewer.IsHorizontalRailEnabled="True" 
                    ScrollViewer.IsDeferredScrollingEnabled="False"
                    Loaded="StatusMessages_Loaded">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsStackPanel VerticalAlignment="Bottom" ItemsUpdatingScrollMode="KeepLastItemInView"/>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
        </ListView>
    </Grid>
</Window>

其他圣歌类

代码语言:javascript
运行
复制
// Allows Win32 access to a Window through WinAPI
public class WindowPrimitives
{
    public IntPtr HWnd { get; }
    private WindowId WindowId { get; }
    public AppWindow AppWindow { get; }
    public Window Window { get; }

    public WindowPrimitives(Window window)
    {
        HWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
        WindowId = Win32Interop.GetWindowIdFromWindow(HWnd);
        AppWindow = AppWindow.GetFromWindowId(WindowId);
        Window = window;
    }
}

// Message Definitions

public class CloseWindowMessage : ValueChangedMessage<bool>
{
    public CloseWindowMessage(bool value) : base(value) { }
}

public class WindowClosedMessage : ValueChangedMessage<bool>
{
    public WindowClosedMessage(bool value) : base(value) { }
}

public class ShutDownMessage : ValueChangedMessage<bool>
{
    public ShutDownMessage(bool value) : base(value) { }
}

public class TraceMessage : ValueChangedMessage<string>
{
    public TraceMessage(string value) : base(value) { }
}

public class TraceMessagesRequest : CollectionRequestMessage<List<string>>
{
}

问题是,在打开TraceMessage之后发送的第一个新的WeakReferenceMessenger.Default.Send()方法中,在RandomMessageGenerator线程和UI线程之间愉快地发送消息的同一个WeakReferenceMessenger.Default.Send()方法抛出了一个“被封送到另一个线程的称为接口的应用程序。(0x8001010E (RPC_E_WRONG_THREAD))”异常导致RandomMessageGenerator线程死亡。

异常由WeakReferenceMessenger.Default.Send(tm);语句在RandomMessageGenerator()方法中抛出。我假设问题在IMessenger接口中(这里涉及的唯一接口)。简单地说,据我所知,这个接口为每种消息类型构建了一个订阅接收者表。然后在每个Send()上对每个接收器发出信号。

一种可能是,接收方列表中的引用都被假定为封送同一个线程。如果是这样的话,那么线程之间的任何消息都不能工作,但是在打开StatusWindow之前就可以了,所以这是不可能的。更改接收者列表显然是可能出现线程问题的地方。由于WeakReferenceMessenger.Default是线程安全的,我认为添加(和删除)注册接收者是线程安全的,但这里似乎并非如此。最后,可能是消息本身(在本例中是string )出错。我不确定,但假设Send方法接受消息的私有副本以传递到封送线程。

你们谁能帮我理解一下我在这里犯的错误吗?

EN

回答 1

Stack Overflow用户

发布于 2022-11-06 17:01:24

我确实找到了解决这个问题的办法。正如预期的那样,这是因为对象是从创建对象的线程以外的线程访问的。

若要修复错误,请添加

代码语言:javascript
运行
复制
    public static DispatcherQueue UIDispatcherQueue = DispatcherQueue.GetForCurrentThread();

App类,它将允许任何线程访问UI线程DispatcherQueue。然后,将StatusWindow.xaml.cs中的消息注册更改为

代码语言:javascript
运行
复制
    // register for Trace messages and, when they arrive, add them to list
    WeakReferenceMessenger.Default.Register<TraceMessage>(this, (r, m) => App.UIDispatcherQueue.TryEnqueue(() => _traceMessages.Add(m.Value)));

现在,这将将消息处理程序中的_traceMessages.Add调用封送到UI线程,这是

代码语言:javascript
运行
复制
    private ObservableCollection<string> _traceMessages { get; } = new();

是建造的。

一旦我意识到抛出异常的点和异常消息都具有欺骗性,这就很容易理解了。虽然消息的发送是接收消息的原因,但真正抛出异常的是试图在错误的线程上处理消息。

无论如何,这似乎表明消息接收处理程序在与发送方相同的线程上执行。我希望文档中的“线程安全”意味着消息被自动编组到接收者的线程中。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/74329304

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档