Debug.Assert()具有意想不到的副作用 - 搜索替代方案

时间:2013-08-04 15:47:11

标签: c# wpf debugging

我经常使用断言来检测意外的程序状态。我认为断言是一个条件消息框,立即停止所有线程,以便(在按“重试”时)我可以检查当前的应用程序状态。

事实并非如此!断言消息打开时,我的wpf应用程序继续处理事件。这是荒谬的,因为在打入调试器时,情况可能与断言“锯”最初的情况完全不同。您可以通过断言本身检查断言的检查是否发生变化,您可以递归执行方法 - 导致多个断言或程序永远无法正常运行的结果。

据我了解断言功能,这是一个设计问题。该对话框在与应用程序本身相同的GUI线程上运行,因此需要为自己的目的处理消息。但这通常具有所描述的副作用。

所以我正在寻找一个断言替代方案,它满足了在调用时停止所有正在运行的线程的要求。作为解决方法,我有时会使用“Debugger.Break();”如果在没有调试器的情况下启动,则(不幸的是)没有效果。

为了说明问题,请参阅以下代码剪切,以最简化的方式产生一些现象:

public partial class MainWindow : Window
{
  int _count = 0;

  public MainWindow()
  {
    InitializeComponent();
  }    
  private void onLoaded(object sender, RoutedEventArgs e)
  {
    test(); 
  }
  protected override void OnLocationChanged(EventArgs e)
  {
    base.OnLocationChanged(e);
  }    
  void test()
  {
    ++_count;
    Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
    {
      test();
    }));

    Trace.TraceInformation(_count.ToString());
    Debug.Assert(_count != 5);
  }
}

在运行代码时,请观看开发人员工作室的输出面板。您将看到数字达到5,然后断言触发。但是当对话框打开时,数字仍在增加。因此断言的条件在断言打开时发生变化! 现在检查主窗口 - 它仍然响应。在“base.OnLocationChanged(e);”处设置断点并移动主窗口=>你会达到突破点。但请注意callstack:

MainWindow.OnLocationChanged(System.EventArgs e)    
(…)    
System.dll!Microsoft.Win32.SafeNativeMethods.MessageBox(System.IntPtr 
System.dll!System.Diagnostics.AssertWrapper.ShowMessageBoxAssert(stri
System.dll!System.Diagnostics.DefaultTraceListener.Fail(string message, str 
System.dll!System.Diagnostics.DefaultTraceListener.Fail(string message)
System.dll!System.Diagnostics.TraceInternal.Fail(string message)
System.dll!System.Diagnostics.Debug.Assert(bool condition)
MainWindow.test()
MainWindow.test.AnonymousMethod__0()

这清楚地表明,在断言打开时可以执行任意代码。

所以我正在寻找一种类似断言的机制来阻止所有现有线程并在其自己的(线程)上下文中运行。 有什么想法吗?

2 个答案:

答案 0 :(得分:4)

您正在了解有关调度程序循环如何工作的更多信息。是的,默认跟踪侦听器用于报告失败的MessageBox对于停止程序没有太大作用。它旨在停止用户,它是一个禁用所有用户输入的模式对话框。但是不会阻止你在代码中做的任何事情。就像它调用Dispatcher.BeginInvoke()。

您需要TraceListener.Fail()方法的另一个实现。这很可能,编辑你的App.xaml.cs文件并使它看起来像这样:

using System.Diagnostics;
...
    public partial class App : Application {
        public App() {
            if (Debugger.IsAttached) {
                var def = Debug.Listeners["Default"];
                Debug.Listeners.Remove(def);
                Debug.Listeners.Add(new MyListener(def));
            }
        }

        private class MyListener : TraceListener {
            private TraceListener defListener;
            public MyListener(TraceListener def) { defListener = def; }
            public override void Write(string message) { defListener.Write(message); }
            public override void WriteLine(string message) { defListener.WriteLine(message); }

            public override void Fail(string message, string detailMessage) {
                base.Fail(message, detailMessage);
                Debugger.Break();
            }
        }
    }

代码的工作原理是从已安装的侦听器中删除DefaultTraceListener,这会让您头疼。并添加一个自定义的MyListener类。哪个做得不多,只需使用原始侦听器来获取“输出”窗口中显示的消息。但是通过覆盖Fail()消息,它会自动触发调试器中断。正是你想要的。

答案 1 :(得分:2)

我正在回答我自己的问题,作为Jon Skeet和Hans Passant提供的信息的摘要和扩展:

对于程序在调试器中运行的情况,使用Debugger.Break()或启用EEMessageException的选项对我来说是可行的。两种方法都会立即停止所有线程。

如果没有调试并且断言发生在GUI线程中,则在单独的线程上运行的消息框会有所帮助(参见http://eprystupa.wordpress.com/2008/07/28/running-wpf-application-with-multiple-ui-threads/

以下是将所有内容放在一起的代码(通过扩展Hans Passant的建议)

  public partial class App : Application
  {
    public App()
    {
      var def = Debug.Listeners["Default"];
      Debug.Listeners.Remove(def);
      Debug.Listeners.Add(new MyListener(def, Dispatcher.CurrentDispatcher));
    }

    private class MyListener : TraceListener
    {
      private TraceListener _defListener;
      private Dispatcher _guiDisp;
      public MyListener(TraceListener def, Dispatcher guiDisp) 
      { 
        _defListener = def;
        _guiDisp = guiDisp;
      }
      public override void Write(string message) { _defListener.Write(message); }
      public override void WriteLine(string message) { _defListener.WriteLine(message); }

      public override void Fail(string message, string detailMessage)
      {
        base.Fail(message, detailMessage);  //write message to the output panel

        if (Debugger.IsAttached)
        {
          //if debugger is attached, just break => all threads stopped
          Debugger.Break();
        }
        else if (Dispatcher.CurrentDispatcher == _guiDisp)
        {
          //running standalone and called in the GUI thread => block it
          Thread anotherGuiThread = new Thread(() =>
          {
            //TODO: nice dlg with buttons
            var assertDlg = new Window() { Width = 100, Height = 100 };
            assertDlg.Show();
            assertDlg.Closed += (s, e) => assertDlg.Dispatcher.InvokeShutdown();
            System.Windows.Threading.Dispatcher.Run();  //run on its own thread
          });

          anotherGuiThread.SetApartmentState(ApartmentState.STA);
          anotherGuiThread.Start();
          anotherGuiThread.Join();
        }
        else
        {
          //running standalone and NOT called in the GUI thread => call normal assert
          _defListener.Fail(message, detailMessage);
        }
      }
    }
  }