TraceListener调用没有阻塞,在调用两次时导致崩溃

时间:2015-02-12 19:14:08

标签: c# .net wpf multithreading

我的设置:TraceListener的简单实现,实例化一次并保留一个实例" MessageBox"实现。 它的目的是监听绑定错误,因为它被添加到PresentationTraceSources.DataBindingSource.Listeners

我的测试用例会打开一个窗口,其中有two个绑定错误,所以我期望的是,跟踪器会启动并显示" MessageBox"窗口通过ShowDialog()。用户单击“确定”,窗口 NOT 已关闭,但隐藏,应出现第二个绑定错误。 相反,我遇到了崩溃:

  

未处理的类型' System.InvalidOperationException'   发生在PresentationFramework.dll

中      
    

" ShowDialog的"只能在隐藏的窗口上调用

  

为什么会发生这种情况?以下是跟踪侦听器的确切实现:

public class DummyListener : TraceListener
{
    private readonly MessageBoxView _window = new MessageBoxView();

    public DummyListener()
    {
        _window.HideOnClose = true;
        _window.DataContext = _window;
    }

    public override void Write( string message )
    {
    }

    public override void WriteLine( string message )
    {
        lock ( _window )
        {
            if ( _window.IsActive )
            {
                // !!! WHY IS THIS CODE EXECUTED
                System.Diagnostics.Debugger.Break();
            }
            _window.Message = message;
            _window.ShowDialog();

            throw new NotImplementedException("This line is never reached");
        }
    }

}

我已经为测试添加了一个锁。对于每个绑定错误,_window.ShowDialog被调用两次然后崩溃。 NotImplementedException也是一个测试,永远不会被调用。所以第一个绑定错误调用_window.ShowDialog(),这似乎阻止,但跟踪侦听器源第二次调用WriteLine,在相同的线程上,它忽略锁定并输入我的Break()调用,如果我继续,则在第一个对话框被隐藏之前调用_window.ShowDialog()。 如果我用_window.ShowDialog替换MessageBox.Show,它确实会阻止。 为什么ShowDialog()表现如此?它会阻止,因为NIE异常没有被抛出,但为什么System.Diagnostics.TraceListener.TraceEvent重新进入锁定部分?

为了澄清:我想池化窗口,我不想生成多个消息框窗口但是按顺序显示它们。 我检查了线程,主线程上发生了一切,但没有任何阻止我期望的方式。

这是一个最小的,完整的,可验证的例子(对不起,它非常详细,因为我无法减少它):

它直接打开一个窗口,由于_window.ShowDialog();它会抛出上面的异常;

App.xaml(删除启动uri)

<Application x:Class="WpfApplicationTracerIssue2.App"

             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             >
    <Application.Resources>

    </Application.Resources>
</Application>

App.xaml.cs

using System;
using System.Diagnostics;
using System.Windows;

namespace WpfApplicationTracerIssue2
{
    public class DummyListener : TraceListener
    {
        private readonly MessageBoxView _window = new MessageBoxView();

        public DummyListener()
        {
            _window.HideOnClose = true;
            _window.DataContext = _window;
        }

        public override void Write( string message )
        {
        }

        public override void WriteLine( string message )
        {
            lock ( _window )
            {
                if ( _window.IsActive )
                {
                    // !!! WHY IS THIS CODE EXECUTED
                    System.Diagnostics.Debugger.Break();
                }
                _window.Title = "Log entry occurred";
                _window.Message = message;
                _window.ShowDialog();

                throw new NotImplementedException("This line is never reached");
            }
        }

    }

    public partial class App : Application
    {
        private DummyListener dl = new DummyListener();

        protected override void OnStartup( StartupEventArgs e )
        {
            base.OnStartup( e );
            Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

            PresentationTraceSources.DataBindingSource.Listeners.Add( dl );

            var d = new MainWindow();

            d.DataContext = d;
            MainWindow = d;
            d.Show(); 
        }
    }
}

MainWindow.xaml(默认为cs)

<Window x:Class="WpfApplicationTracerIssue2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Label Content="{Binding A}"></Label>
        <Label Content="{Binding B}"></Label>
    </StackPanel>
</Window>

最后是虚拟消息框视图:

MessageBoxView.xaml

<Window x:Class="WpfApplicationTracerIssue2.MessageBoxView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MessageBoxView" Height="300" Width="300">
    <StackPanel>
        <Label Content="{Binding Message}" ContentStringFormat="msg: {0}"></Label>
        <Button Click="ButtonBase_OnClick">Ok</Button>
    </StackPanel>
</Window>

MessageBoxView.xaml.cs

using System.Windows;

namespace WpfApplicationTracerIssue2
{

    public partial class MessageBoxView : Window
    {
        public static readonly DependencyProperty MessageProperty = DependencyProperty.Register("Message", typeof ( string ), typeof ( MessageBoxView ), new PropertyMetadata( default( string ) ) );

        public string Message{ get { return ( string ) GetValue( MessageProperty ); } set { SetValue( MessageProperty, value ); }}


        public MessageBoxView(){InitializeComponent();}

        public bool HideOnClose { get; set; }


        private void ButtonBase_OnClick( object sender, RoutedEventArgs e )
        {
            if ( HideOnClose ){ Hide(); }
            else{ Close(); }
        }
    }
}

1 个答案:

答案 0 :(得分:1)

首先,这似乎与多线程无关。这真的是关于单个线程中的重新入侵。

问题是ShowDialog()方法必然包含线程消息的消息泵。侦听器框架使用线程消息队列将事件传递给侦听器。因此,当您显示第一条消息的窗口时,ShowDialog()方法中的消息泵将继续并在第一条消息的窗口被解除之前传递第二条消息。

解决此问题的一种方法是更改​​侦听器代码,使其自身对消息进行排队,并一次显示它们。这可能看起来像这样:

public class DummyListener : TraceListener
{
    private readonly MessageBoxView _window = new MessageBoxView();
    private readonly BlockingCollection<string> _messages = new BlockingCollection<string>();

    public DummyListener()
    {
        _window.HideOnClose = true;
        _window.DataContext = _window;

        PresentMessages();
    }

    private async void PresentMessages()
    {
        IEnumerator<string> enumerator = _messages.GetConsumingEnumerable().GetEnumerator();

        while (await Task.Factory.StartNew(() => enumerator.MoveNext(), TaskCreationOptions.LongRunning))
        {
            _window.Title = "Log entry occurred";
            _window.Message = enumerator.Current;
            _window.ShowDialog();
        }
    }

    public override void Write(string message)
    {
    }

    public override void WriteLine(string message)
    {
        _messages.Add(message);
    }

    protected override void Dispose(bool disposing)
    {
        _messages.CompleteAdding();
        base.Dispose(disposing);
    }
}

上面使用BlockingCollection<string>对每条消息进行排队。此外,在构造函数中,它调用一个async方法来处理窗口的实际呈现。

PresentMessages()方法使用异步任务等待消息可用。当该任务完成时,该方法将在原始线程中恢复以显示窗口。

注意:为了使上述工作正常,您需要确保在UI线程中创建了侦听器类实例本身,以便PresentMessages()方法中的延续实际上是也在UI线程中执行。为了确保这一点,我更改了您的App代码,使其看起来像这样:

public partial class App : Application
{
    private DummyListener dl;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

        dl = new DummyListener();
        PresentationTraceSources.DataBindingSource.Listeners.Add(dl);

        var d = new MainWindow();

        d.DataContext = d;
        MainWindow = d;
        d.Show();
    }
}

另请注意,我使用Task.Factory.StartNew()创建任务,而不是更简洁Task.Run()。这允许我传递TaskCreationOptions.LongRunning,以便Task类知道此操作可能需要很长时间。这样就可以避免在阻塞操作上占用线程池线程,这可能需要很长时间才能完成。

更好的方法是实现异步完成MoveNext()方法(即IEnumerable<T>的异步版本)。我把它作为读者的练习。 :)