2016-02-28 7 views
0

Текущая ситуацияПоказать BusyIndicator при инициализации элементы управления UserControl (WPF + MVVM + DataTemplate приложений)

Я использую следующий подход для решения View для согласования ViewModel. (Упрощенный)

<Window.Resources> 
    <ResourceDictionary> 
     <DataTemplate DataType="{x:Type local:DemoVm2}"> 
      <local:DemoViewTwo /> 
     </DataTemplate> 
     <DataTemplate DataType="{x:Type local:DemoVm}"> 
      <local:DemoView /> 
     </DataTemplate> 
    </ResourceDictionary> 
</Window.Resources> 

<DockPanel LastChildFill="True"> 
    <Button Content="Switch To VmOne" Click="ButtonBase_OnClick"></Button> 
    <Button Content="Switch To VmTwo" Click="ButtonBase_OnClick2"></Button> 

    <ContentPresenter Content="{Binding CurrentContent}" /> 
</DockPanel> 

Взгляды получить автоматически разрешаться WPF после переключения ViewModel внутри ContentPresenter.

При использовании сложного вида, который может занять 2-4 секунды для инициализации, я хочу отобразить BusyIndicator. Они занимают до 2-4 секунд из-за количества визуальных данных NOT.

Проблема

Я не знаю, когда View-х завершили процесс инициализации/загрузки, потому что имеют доступ только к текущей ViewModel.

Мой подход

Моя идея в том, чтобы присоединить поведение к каждому UserControl, который может установить логическое значение к их присоединенной ViewModel (IsBusy = ложь) после InitializeComponent() обработанную или обрабатывать их LoadedEvent. Это свойство может быть привязано к BusyIndicator в другом месте.

Я не очень доволен этим решением, потому что мне нужно будет приложить это поведение к каждому отдельному Usercontrol/view.

У кого-нибудь есть другое решение для такого рода проблем? Наверное, я не единственный, кто хочет скрыть процесс загрузки графического интерфейса пользователя?

Я недавно натолкнулся на эту тему http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx. Но так как это с 2007 года, могут быть лучшие/более удобные способы достижения моей цели?

+0

Кто-то меня исправит, если я ошибаюсь, но, думаю, вы находитесь в плохом месте. Индикатор занятости должен запускаться в потоке пользовательского интерфейса, но контроль инициализации также выполняется в потоке пользовательского интерфейса. Я не думаю, что ваш индикатор занятости обновится в течение от 2 до 4 секунд, чтобы инициализировать ваше представление. –

+0

Я не знаю, как ребята DevExpress справились с этим сценарием, но я видел, что их WPF LoadingDecorator делает что-то подобное. Он украшает каждый ChildControl, пока они загружаются. Реализация такого декоратора также будет работать, но я думаю, мне придется вставлять его в каждый Datatemplate. – KroaX

+0

Альтернатива: http://stackoverflow.com/questions/3601125/wpf-tabcontrol-preventing-unload-on-tab-changeI У меня тоже была медленная проблема загрузки визуального дерева, но после перехода в кешированный ContentPresenter я очень доволен. – Peter

ответ

-1

Альтернативный подход заключается в том, чтобы начать с UserControl hidden и IsBusy на true. Запустите загрузку в отдельном потоке на Application.Dispatcher. Конечные утверждения протектора IsBusy = false; UserControl.Visibility = Visibility.Visible;

+0

WPF визуальное дерево не может быть инициализировано из разных потоков. Вы получите InvalidOperationException –

0

Существует нет простого и универсального решения этой проблемы. В каждом конкретном случае вы должны написать собственную логику для инициализации визуального дерева без блокировки.

Ниже приведен пример реализации инициализации без блокировки ListView с индикатором инициализации.

UserControl, который содержит ListView и инициализации индикатора:

XAML:

<UserControl x:Class="WpfApplication1.AsyncListUserControl" 
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:WpfApplication1" 
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"> 
    <Grid Margin="5" Grid.Row="1"> 
     <ListView x:Name="listView"/> 
     <Label x:Name="itemsLoadingIndicator" Visibility="Collapsed" Background="Red" HorizontalAlignment="Center" VerticalAlignment="Center">Loading...</Label> 
    </Grid> 
</UserControl> 

CS:

public partial class AsyncListUserControl : UserControl 
{ 
    public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(IEnumerable), typeof(AsyncListUserControl), new PropertyMetadata(null, OnItemsChanged)); 

    private static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     AsyncListUserControl control = d as AsyncListUserControl; 
     control.InitializeItemsAsync(e.NewValue as IEnumerable); 
    } 

    private CancellationTokenSource _itemsLoadiog = new CancellationTokenSource(); 
    private readonly object _itemsLoadingLock = new object(); 

    public IEnumerable Items 
    { 
     get 
     { 
      return (IEnumerable)this.GetValue(ItemsProperty); 
     } 
     set 
     { 
      this.SetValue(ItemsProperty, value); 
     } 
    } 

    public AsyncListUserControl() 
    { 
     InitializeComponent(); 
    } 

    private void InitializeItemsAsync(IEnumerable items) 
    { 
     lock(_itemsLoadingLock) 
     { 
      if (_itemsLoadiog!=null) 
      { 
       _itemsLoadiog.Cancel(); 
      } 

      _itemsLoadiog = new CancellationTokenSource(); 
     } 

     listView.IsEnabled = false; 
     itemsLoadingIndicator.Visibility = Visibility.Visible; 
     this.listView.Items.Clear(); 

     ItemsLoadingState state = new ItemsLoadingState(_itemsLoadiog.Token, this.Dispatcher, items); 

     Task.Factory.StartNew(() => 
     { 
      int pendingItems = 0; 
      ManualResetEvent pendingItemsCompleted = new ManualResetEvent(false); 

      foreach(object item in state.Items) 
      { 
       if (state.CancellationToken.IsCancellationRequested) 
       { 
        pendingItemsCompleted.Set(); 
        return; 
       } 

       Interlocked.Increment(ref pendingItems); 
       pendingItemsCompleted.Reset(); 

       state.Dispatcher.BeginInvoke(
        DispatcherPriority.Background, 
        (Action<object>)((i) => 
        { 
         if (state.CancellationToken.IsCancellationRequested) 
         { 
          pendingItemsCompleted.Set(); 
          return; 
         } 

         this.listView.Items.Add(i); 
         if (Interlocked.Decrement(ref pendingItems) == 0) 
         { 
          pendingItemsCompleted.Set(); 
         } 
        }), item); 
      } 

      pendingItemsCompleted.WaitOne(); 
      state.Dispatcher.Invoke(() => 
      { 
       if (state.CancellationToken.IsCancellationRequested) 
       { 
        pendingItemsCompleted.Set(); 
        return; 
       } 

       itemsLoadingIndicator.Visibility = Visibility.Collapsed; 
       listView.IsEnabled = true; 
      }); 
     }); 
    } 

    private class ItemsLoadingState 
    { 
     public CancellationToken CancellationToken { get; private set; } 
     public Dispatcher Dispatcher { get; private set; } 
     public IEnumerable Items { get; private set; } 

     public ItemsLoadingState(CancellationToken cancellationToken, Dispatcher dispatcher, IEnumerable items) 
     { 
      CancellationToken = cancellationToken; 
      Dispatcher = dispatcher; 
      Items = items; 
     } 
    } 
} 

Пример:

<Window x:Class="WpfApplication1.MainWindow" 
     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" 
     xmlns:local="clr-namespace:WpfApplication1" 
     mc:Ignorable="d" 
     Title="MainWindow" Height="350" Width="525"> 
    <Window.DataContext> 
     <local:MainWindowViewModel/> 
    </Window.DataContext> 
    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="Auto"/> 
      <RowDefinition/> 
     </Grid.RowDefinitions> 
     <Button Content="Load Items" Command="{Binding LoadItemsCommand}" /> 
     <local:AsyncListUserControl Grid.Row="1" Items="{Binding Items}"/> 
    </Grid> 
</Window> 

ViewModel:

using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Windows.Input; 

namespace WpfApplication1 
{ 
    public class MainWindowViewModel:INotifyPropertyChanged 
    { 
     private readonly ICommand _loadItemsCommand; 
     private IEnumerable<string> _items; 

     public event PropertyChangedEventHandler PropertyChanged; 

     public MainWindowViewModel() 
     { 
      _loadItemsCommand = new DelegateCommand(LoadItemsExecute); 
     } 

     public IEnumerable<string> Items 
     { 
      get { return _items; } 
      set { _items = value; OnPropertyChanged(nameof(Items)); } 
     } 

     public ICommand LoadItemsCommand 
     { 
      get { return _loadItemsCommand; } 
     } 

     private void LoadItemsExecute(object p) 
     { 
      Items = GenerateItems(); 
     } 

     private IEnumerable<string> GenerateItems() 
     { 
      for(int i=0; i<10000; ++i) 
      { 
       yield return "Item " + i; 
      } 
     } 

     private void OnPropertyChanged(string propertyName) 
     { 
      var h = PropertyChanged; 
      if (h!=null) 
      { 
       PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
      } 
     } 

     public class DelegateCommand : ICommand 
     { 
      private readonly Predicate<object> _canExecute; 
      private readonly Action<object> _execute; 
      public event EventHandler CanExecuteChanged; 

      public DelegateCommand(Action<object> execute) : this(execute, null) { } 

      public DelegateCommand(Action<object> execute, Predicate<object> canExecute) 
      { 
       _execute = execute; 
       _canExecute = canExecute; 
      } 

      public bool CanExecute(object parameter) 
      { 
       if (_canExecute == null) 
       { 
        return true; 
       } 

       return _canExecute(parameter); 
      } 

      public void Execute(object parameter) 
      { 
       _execute(parameter); 
      } 

      public void RaiseCanExecuteChanged() 
      { 
       if (CanExecuteChanged != null) 
       { 
        CanExecuteChanged(this, EventArgs.Empty); 
       } 
      } 
     } 
    } 
} 

Основные черты этого подхода: свойства зависимостей

  1. Пользовательские для данных, которые требует много UI инициализации.

  2. DependencyPropertyChanged callback начинает рабочий поток, который управляет Инициализация пользовательского интерфейса.

  3. Рабочий поток отправляет небольшие действия с низким приоритетом выполнения в поток пользовательского интерфейса, который несут ответственность пользовательского интерфейса.

  4. Дополнительная логика для сохранения согласованного состояния в случае, когда инициализация выполняется снова, пока предыдущая инициализация не завершена .

+0

Вы вложили много работы в свой ответ. К сожалению, это не справляется с моей проблемой. Мой вопрос касался украшения индикатора длинной загрузки UserControl, которая просто вызывает InitializeComponents() без загрузки данных. Поэтому просто скройте процесс рендеринга. – KroaX

+0

Мне интересно, насколько большим должно быть статическое визуальное дерево, чтобы иметь такое длительное время загрузки. Вы пытались запустить просмотр без привязки DataContext и измерения времени? –

 Смежные вопросы

  • Нет связанных вопросов^_^