使用页面加载另一个线程,以防止在 WPF 中渲染内容时 UI 冻结

问题描述 投票:0回答:1

我是 WPF 和 C# 新手,我遇到了这个问题,我有一个加载微调器和一个在框架内加载新页面的按钮。

我想要的是将旋转器渲染在框架顶部,并在另一个线程上加载页面(该页面包含一个数据网格,其中有大量数据使UI冻结),这样旋转器可以旋转而不会冻结,然后当页面完成渲染数据网格上的所有项目时消失。

到目前为止我有这个:

private void StackPanel_Click(object sender, RoutedEventArgs e)
{
    LoadingSpinner.Visibility = Visibility.Visible;

    var ClickedButton = e.Source as UserControls.MenuButton;
    if (ClickedButton.Command != NavigationCommands.BrowseBack && ClickedButton.NavUri != null)
    {
        Page page = (Page)Application.LoadComponent(new Uri(ClickedButton.NavUri.ToString(), UriKind.Relative));
        page.DataContext = DataContext;
        pgContenedorConfiguracion.NavigationService.Navigate(page);
    }

    LoadingSpinner.Visibility = Visibility.Collapsed;
}

这段代码有两个问题:

  1. 旋转器立即消失。
  2. 如果我删除
    LoadingSpinner.Visibility = Visibility.Collapsed;
    ,旋转器就会开始冻结,然后,当页面完成渲染时,它开始旋转。

然后我想出了这段代码:

private void StackPanel_Click(object sender, RoutedEventArgs e)
{

    LoadingSpinner.Visibility = Visibility.Visible;

    var ClickedButton = e.Source as UserControls.MenuButton;
    if (ClickedButton.Command != NavigationCommands.BrowseBack && ClickedButton.NavUri != null)
    {
        Dispatcher.Invoke(DispatcherPriority.Background, new Action(() =>
        {
            Page page = (Page)Application.LoadComponent(new Uri(ClickedButton.NavUri.ToString(), UriKind.Relative));
            page.DataContext = DataContext;
            pgContenedorConfiguracion.NavigationService.Navigate(page);
        }));
    }

    Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() =>
    {
        LoadingSpinner.Visibility = Visibility.Collapsed;
    }));
}

它有点有效,现在旋转器出现在开头,不会立即消失,但它冻结了,并且在页面渲染后以某种方式消失了......有人可以向我解释这里发生了什么吗?我知道调度员会优先考虑,但仅此而已。

有什么最好的方法来做我想做的事吗?

c# wpf multithreading user-interface dispatcher
1个回答
0
投票

您无法在不同线程上加载

Page
,因为
Page
DispatcherObject
,因此具有线程关联性。
您想要做的是加载数据并在不同线程上或异步执行初始化。

应尽可能避免使用对象进行导航,因为这会影响性能。 WPF 导航控件保存历史记录以启用向后和向前导航。如果您使用对象进行导航,控件会将完整的对象保留在内存中。但如果您通过 URI 导航,内存占用量会显着降低(由于实例化,加载可能需要更长的时间)。

如果您想在导航主机中重用

Page
实例以缩短加载时间(如有必要),那么您还应该将引用存储在导航器类中,而不是每次都创建新实例。
通过 URI 导航或存储对重用
Page
实例的引用。

要禁用繁忙的微调器,您必须等待初始化完成。否则,你就会过早地隐藏它。我建议为此目的收听一个活动。

此外,

Page
提供了链接到父导航主机的
NavigationService
实例。您不需要在页面中引用任何导航主机。这只会导致糟糕的设计决策。

private readonly myPage = new MyPage();

private void ShowSpinner()
  => LoadingSpinner.Visibility = Visibility.Visible;

private void HideSpinner()
  => LoadingSpinner.Visibility = Visibility.Collapsed;

private void StackPanel_Click(object sender, RoutedEventArgs e)
{
  ShowSpinner();

  var clickedButton = e.Source as UserControls.MenuButton;
  if (clickedButton.Command != NavigationCommands.BrowseBack 
    && clickedButton.NavUri != null)
  {
    // Get notified when the navigation to the page being completed 
    // (Page is created but not loaded at this point).
    // We then can observe the page and wait for it to complete the initialization.
    this.NavigationService.Navigated += OnPageNavigated;
    
    // Don't create new instance on each call
    //Page page = (Page)Application.LoadComponent(new Uri(ClickedButton.NavUri.ToString(), UriKind.Relative));

    // DataContext is inherited. The following line is redundant.
    //page.DataContext = DataContext;

    // Instead, reuse the page instance
    this.NavigationService.Navigate(this.myPage);

    // However, if you need a fresh instance, then navigate by URI
    this.NavigationService.Navigate(new Uri(ClickedButton.NavUri.ToString(), UriKind.Relative));
  }
}

private void OnPageNavigated(object sender, NavigationEventArgs e)
{
  this.NavigationService.Navigated -= OnPageNavigated;

  var myPage = (MyPage)e.Content;  
  if (myPage.IsInitializing)
  {
    myPage.InitializationCompleted += OnPageInitializationCompleted;
  }
  else
  {
    HideSpinner();
  }
}

private void OnPageInitializationCompleted(object sender, NavigationEventArgs e)
{
  var myPage = (MyPage)sender;  
  myPage.InitializationCompleted += OnPageInitializationCompleted;

  HideSpinner();
}

视图中异步或并发初始化的一个好模式是让控件实现一个

InitializeAsync
方法,然后从
Loaded
事件中调用该方法:

partial class MyPage : Page
{
  public event EventHandler InitializationCompleted;
  public bool IsInitializing { get; private set; }

  public MyPage()
  {
    InitializeComponent();

    this. Loaded += OnLoaded;
  }

  private async void OnLoaded(object sender, RoutedEventArgs e)
    => await InitializeAsnyc();

  private async Task InitializeAsnyc()
  {
    this.IsInitializing = true;

    // Preferably call async APIs
    await GetDataAsymc();

    // You can offload long running and CPU intensive work 
    // to a background thread.
    // If not awaited, the context immediately continues while the Task.Run operation 
    // executes in the background
    await Task.Run(DoSomethingInTheBackground);

    // TODO::Finalize initialization


    this.IsInitializing = false;
    OnInitializationCompleted();
  }

  // The CPU intensive and long running operation
  private void DoSomethingInTheBackground()
  {
  }

  protected virtual void OnInitializationCompleted(object sender, RoutedEventArgs e)
    => this.InitializationCompleted?.Invoke(this, EventArgs.Empty);
}
© www.soinside.com 2019 - 2024. All rights reserved.