如果来自后台任务的ConsoleWriteLine太多,则Wpf TextBoxOutputter UI任务崩溃

iaqfqrcu  于 2023-04-22  发布在  其他
关注(0)|答案(1)|浏览(120)

我想建立一个应用程序,有一个WPF用户界面和做一些后台任务与文件转换。显示操作的进展,我使用TexBoxOutputter。
我已经将控制台输出链接到我的TexBoxOutputter并将两个线程与调度器链接。现在我可以在后台任务中使用Console.Writeline命令,以便写入UI,我现在尝试转换哪个文件。
这个过程一般工作良好。但现在的要求是在一个步骤中转换大约500个文件的数量。后台程序非常快,所以这是在大约30s用于整个500个文件。现在的问题是,如果我在每个文件后将文件的名称写入控制台,我的UI线程库存和崩溃,整个过程需要更长的时间。我的猜测是,原因是console.WriteLine超载我的UI线程。
那么,如果我想在我的UI上显示相同的功能,但以更稳定,至少更快的方式转换哪个文件,我需要改变什么?
下面是一些代码片段:TesxBox输出:

public class TextBoxOutputter : TextWriter
    {
        volatile TextBox textBox = null;
        int count = 0;

        public  TextBoxOutputter(TextBox output)
        {
            textBox = output;
        }

        public override void Write(char value)
        {
            base.Write(value);
            UpdateTextBox(value.ToString());
 
        }

        private void UpdateTextBox(string text)
        {
            if (textBox.Dispatcher.CheckAccess())
            {
                textBox.AppendText(text);              
            }
            else
            {
                textBox.Dispatcher.BeginInvoke(new Action(() =>
                {
                    
                    textBox.AppendText(text);
                    count = count + 1;
                    //test Scroll down only after 30 lines added because otherwhise main UI thread will be locked.
                    if (count >= 30)
                    {
                        count = 0;
                        textBox.ScrollToEnd();
                        
                    }
                    
                }));
            }
        }

        public override Encoding Encoding
        {
            get { return System.Text.Encoding.UTF8; }
        }
    }
}

UI控制台链接到输出器:

outputter = new TextBoxOutputter(ConsoleOutput);
            Console.SetOut(outputter);

线程:

void StartProcess(object sender, RoutedEventArgs e)
        {
            
            worker.WorkerReportsProgress = true;
            worker.WorkerSupportsCancellation = true;
          
            if (SourcePath != null)
            {
                worker = new BackgroundWorker(); 
                worker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker2_DoWork);
            }           
            TheProgressbar.IsIndeterminate = true;
            worker.RunWorkerAsync();

        }
        private Thread _thread = null;

        private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e)
        {
            ThreadStart threadStart = new ThreadStart(StartConversion);
            _thread = new Thread(threadStart);
            _thread.SetApartmentState(ApartmentState.STA); 
            _thread.Start();
        }
hgqdbh6s

hgqdbh6s1#

主要问题是TextBox的性能不佳。
如果你需要输入和输出,你必须创建一个组合控件,其中包含一个ListBox用于输出,一个TextBox用于输入。好处是ListBox提供了UI虚拟化。添加一个项目应该比操作一个长的多行字符串更快。
如果你覆盖了ListBoxItem的模板,你就可以删除交互行为(比如鼠标悬停效果)。这会让ListBox看起来像TextBox
这个想法是将ListBox堆叠在TextBox之上。结果是一个经典的文本控制台,只有一行文本输入。当用户按下回车键时,您将输入添加到反向的(上下颠倒)ListBox,然后清除TextBox。结果是发送行向上移动,而输入行保持在底部。就像经典的控制台一样。
下面的例子展示了这样一个简单的文本控制台。TextConsole能够在按下Enter键或通过SendInputCommand(路由命令)发送输入(向上滚动)。它还允许将字符串的源集合绑定到TextConsole.ConsoleOutputSource属性。
这使您可以更新控制台,而无需发送显式用户输入(例如报告进度)。

TextConsole.cs

[TemplatePart(Name = "PART_TextInput", Type = typeof(TextBoxBase))]
[TemplatePart(Name = "PART_OutputPresenter", Type = typeof(ItemsControl))]
public class TextConsole : Control
{
  public ObservableCollection<string> ConsoleOutputSource
  {
    get => (ObservableCollection<string>)GetValue(ConsoleOutputSourceProperty);
    set => SetValue(ConsoleOutputSourceProperty, value);
  }

  public static readonly DependencyProperty ConsoleOutputSourceProperty = DependencyProperty.Register(
    "ConsoleOutputSource",
    typeof(ObservableCollection<string>),
    typeof(TextConsole),
    new PropertyMetadata(default(ObservableCollection<string>), OnConsoleOutputSourceChanged));

  private static void OnConsoleOutputSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    => (d as TextConsole).OnConsoleOutputSourceChanged((ObservableCollection<string>)e.OldValue, (ObservableCollection<string>)e.NewValue);

  public static RoutedUICommand SendInputCommand { get; }
  private ItemsControl? PART_OutputPresenter { get; set; }
  private TextBox? PART_TextInput { get; set; }
  private ScrollViewer? OutputPresenterScrollViewer { get; set; }

  static TextConsole()
  {
    TextConsole.DefaultStyleKeyProperty.OverrideMetadata(typeof(TextConsole), new FrameworkPropertyMetadata(typeof(TextConsole)));
    TextConsole.SendInputCommand = new RoutedUICommand("Commit console input command", nameof(TextConsole.SendInputCommand), typeof(TextConsole));
  }

  public TextConsole()
  {
    this.ConsoleOutputSource = new ObservableCollection<string>();
    var sendInputKeyBinding = new KeyBinding(TextConsole.SendInputCommand, Key.Enter, ModifierKeys.None);
    _ = this.InputBindings.Add(sendInputKeyBinding);
    var sendInputCommandBinding = new CommandBinding(TextConsole.SendInputCommand, ExecuteSendInputCommand, CanExecuteSendInputCommand);
    _ = this.CommandBindings.Add(sendInputCommandBinding);
  }

  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    this.PART_OutputPresenter = GetTemplateChild("PART_OutputPresenter") as ItemsControl;
    if (this.PART_OutputPresenter is not null)
    {
      this.PART_OutputPresenter.Loaded += OnItemsControlLoaded;
      this.PART_OutputPresenter.ItemsSource = this.ConsoleOutputSource;
    }

    this.PART_TextInput = GetTemplateChild("PART_TextInput") as TextBox;
    if (this.PART_TextInput is not null)
    {
      this.PART_TextInput.AcceptsReturn = false;
    }
  }

  protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  {
    base.OnMouseLeftButtonDown(e);
    _ = this.Dispatcher.InvokeAsync(() => _ = Keyboard.Focus(this.PART_TextInput));
  }

  private void OnItemsControlLoaded(object sender, EventArgs e)
  {
    if (TryFindVisualChildElement(this.PART_OutputPresenter, out Panel? itemsHost))
    {
      // Make items appear from bottom to top
      itemsHost.VerticalAlignment = VerticalAlignment.Bottom;
    }

    if (TryFindVisualChildElement(this.PART_OutputPresenter, out ScrollViewer? scrollViewer))
    {
      this.OutputPresenterScrollViewer = scrollViewer;
    }
  }

  protected virtual void OnConsoleOutputSourceChanged(ObservableCollection<string> oldValue, ObservableCollection<string> newValue)
  {
    if (this.PART_OutputPresenter is not null)
    {
      this.PART_OutputPresenter.ItemsSource = this.ConsoleOutputSource;
    }
  }

  private void CanExecuteSendInputCommand(object sender, CanExecuteRoutedEventArgs e)
    => e.CanExecute = !string.IsNullOrWhiteSpace(this.PART_TextInput?.Text);

  private void ExecuteSendInputCommand(object sender, ExecutedRoutedEventArgs e)
  {
    if (this.PART_TextInput is not null)
    {
      SendInput(this.PART_TextInput.Text);
      this.PART_TextInput.Clear();
    }
  }

  private void SendInput(string input)
  {
    this.ConsoleOutputSource?.Add(input);
    this.OutputPresenterScrollViewer?.ScrollToBottom();
  }

  private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild? resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;

    if (parent is Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (int childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

      if (childElement is TChild child)
      {
        resultElement = child;
        return true;
      }

      if (TryFindVisualChildElement(childElement, out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

泛型.xaml

<Style TargetType="{x:Type local:TextConsole}">
  <Setter Property="BorderThickness"
          Value="1" />
  <Setter Property="BorderBrush"
          Value="Gray" />
  <Setter Property="Background"
          Value="Transparent" />
  <Setter Property="Cursor"
          Value="IBeam" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:TextConsole}">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition />
              <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <ListBox x:Name="PART_OutputPresenter"
                     Grid.Row="0"
                     BorderThickness="0"
                     Focusable="False"
                     IsHitTestVisible="False">
              <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                  <Setter Property="Focusable"
                          Value="False" />
                  <Setter Property="IsHitTestVisible"
                          Value="False" />
                  <Setter Property="Template">
                    <Setter.Value>
                      <ControlTemplate TargetType="ListBoxItem">
                        <ContentPresenter />
                      </ControlTemplate>
                    </Setter.Value>
                  </Setter>
                </Style>
              </ListBox.ItemContainerStyle>
            </ListBox>
            <TextBox x:Name="PART_TextInput"
                     Grid.Row="1"
                     BorderThickness="0" />
          </Grid>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

此外,你必须修复你的线程代码。你应该检查你用来转换文件的API是否公开了一个异步API,该API启用了自然的async/await以增强性能。
这样的API比多线程更受欢迎。下面的代码是修改后的原始代码版本,可以使用TextConsole控件。

主窗口.xaml

<Window>
  <local:TextConsole Height="400"
                     ConsoleOutputSource="{Binding TextConsoleWriter.ConsoleOutputSource}" />
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public TextConsoleWriter TextConsoleWriter { get; }

  public MainWindow()
  {
    InitializeComponent();

    this.DataContext = this;
    this.TextConsoleWriter = new TextConsoleWriter();
  }

  private void StartProcess(object sender, RoutedEventArgs e)
  {
    if (this.SourcePath == null)
    {
      return;
    }
      
    this.Progressbar.IsIndeterminate = true;

    // Create a background thread for the conversion job.
    // Note, that if you can use an async API to have asynchronous conversion,
    // you should prefer this over a background thread.
    // In case you need to do something once the conversion has completed (e.g. clean up),
    // simply await the Task.
    // If you don't await the Task you should consider to use a SemaphoreSlim
    // to restrict the number or concurrent operations (e.g., max two background thraeds).
    // If you only allow one operation, simply await the Task
    Task.Run(StartConversion);
  }

  private void StartConversion()
  {
    // Pseudo code
    foreach (FileInfo fileInfo in fileInfosToConvert)
    {
      // Report the progress.
      // You can try different DispatcherPriority values to find which one
      // reliefs some pressure without deferring too much.
      // DispatcherPriority.Background is a good starting point.
      this.Dispatcher.InvokeAsync(
        () => this.TextConsoleWriter.AppendLine(fileInfo.Name), 
        System.Windows.Threading.DispatcherPriority.Background);
    }
  }
}

TextConsoleWriter.cs

ublic class TextConsoleWriter : INotifyPropertyChanged
{
  public ObservableCollection<string> ConsoleOutputSource { get; }
  public event PropertyChangedEventHandler? PropertyChanged;

  public TextConsoleWriter()
  {
    this.ConsoleOutputSource = new ObservableCollection<string>();
  }

  public void AppendLine(string line)
  {
    this.ConsoleOutputSource.Add(line);
  }
}

相关问题