0

Current Situation

I am using the following approach to resolve a View for a matching ViewModel. (simplified)

<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>

The Views get automatically resolved by WPF after switching the ViewModel inside the ContentPresenter.

When using complex View’s that may take 2-4 seconds for initialization I want to display a BusyIndicator. They take up to 2-4 seconds because of the amount of visuals NOT data.

Problem

I don’t know when the View’s have finished their initialization/loading process because I only have access to the current ViewModel.

My approach

My Idea was to attach a behavior to each UserControl that may set a boolean Value to the their attached ViewModel (IsBusy=false) after InitializeComponent() finished or handle their LoadedEvent. This Property could be bound to a BusyIndicator elsewhere.

I am not really satisfied with this solution because I would need to attach this behavior to each individual Usercontrol/view.

Does anyone have another solution for this kind of problem? I guess I am not the only one who wants to hide the GUI loading process from the user?!

I recently came across this Thread http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx . But since this is from 2007 there might be some better / more conveniant ways to achieve my goal?

Mohnkuchenzentrale
  • 5,745
  • 4
  • 30
  • 41
  • Somebody will correct me if I am wrong but I think you are in a bad spot here. The busy indicator would have to run on the UI thread but control initialization runs on the UI thread as well. I don't think your busy indicator will update for the 2 to 4 seconds it takes to initialize your view. – Jason Boyd Feb 28 '16 at 16:54
  • I don't know how the DevExpress guys have handled this scenario but I have seen their WPF LoadingDecorator does something similar. It decorates every ChildControl as long as they are Loading. Implementing such a decorator would also work but I guess I would have to insert it inside every Datatemplate. – Mohnkuchenzentrale Feb 28 '16 at 17:05
  • Alternative: http://stackoverflow.com/questions/3601125/wpf-tabcontrol-preventing-unload-on-tab-changeI I too had slow visual tree loading issue but after switching to cached ContentPresenter, I am very happy. – Peter Feb 28 '16 at 19:18

2 Answers2

1

There is no easy and universal solution for this problem. In each concrete case you should write custom logic for non blocking visual tree initialization.

Here is an example how to implement non blocking initialization of ListView with initializing indicator.

UserControl that contains ListView and initializing indicator:

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;
        }
    }
}

Usage example:

<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);
                }
            }
        }
    }
}

The main features of this approach:

  1. Custom dependency properties for data that requires much UI initialization.

  2. DependencyPropertyChanged callback starts worker thread that manages UI initialization.

  3. Worker thread dispatches small actions with low execution priority into UI thread which keeps UI responsible.

  4. Additional logic to keep consistent state in case when initialization executed again while previous initialization is not completed yet.

nicolas2008
  • 945
  • 9
  • 11
  • You invested lots of work into your answer. Sadly it doesn't handle my problem. My Question addressed the indicator decoration of a long loading UserControl that does just call InitializeComponents() without loading dataitems. So just hide the renderingprocess. – Mohnkuchenzentrale Feb 28 '16 at 21:03
  • I'm wondering how large should be static visual tree to have such a long loading time. Did you try to run view without binding DataContext and measure time? – nicolas2008 Feb 28 '16 at 21:23
-1

An alternate approach is to start with UserControl hidden and IsBusy on true. Start the loading in a separate thread on the Application.Dispatcher. The final statements of the tread are IsBusy=false; UserControl.Visibility = Visibility.Visible;