0

I have my xaml:

        <Window x:Class="Views.ShellView"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
                xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                Height="768" Width="1024" WindowStartupLocation="CenterScreen"
                Title="{Binding Path=DisplayName}">

            <xctk:BusyIndicator x:Name="BusyIndicator"  IsBusy="{Binding IsBusy, UpdateSourceTrigger=PropertyChanged}" >

                <TreeView Style="{StaticResource TableSchemaTreeViewStyle}" ItemContainerStyle="{StaticResource SchemaTreeViewStyle}" Margin="0,15,0,0" 
                                  x:Name="TreeViewSchema" 
                                  TreeViewItem.Expanded="TreeViewSchema_OnExpanded" 
                                  TreeViewItem.Selected="TreeViewSchema_OnSelected" 
                                  Grid.Row="2" 
                                  ItemsSource="{Binding CurrentProject.Tables, Converter={StaticResource TreeViewSortingConverter}, ConverterParameter=FullTableName}">
                </TreeView>

            </xctk:BusyIndicator>
        </Window>

And suppose I have long running task in code-behind which is performed on UI thread (long filtering of treeview, it can have more then 1000 tables and each table more than 100 columns in it).

Lets say I iterate and set tableTreeViewItem.Visibility = Visibility.Collapsed; for each item.

What I want: show BusyIndicator by setting it to true before this action:

BusyIndicator.IsBusy = true;.

The problem: both actions on UI thread and binding does not work as expected. I tried few things:

BusyIndicator.IsBusy = true;

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

Task.Factory.StartNew(() =>
{

    tableTreeViewItem.Visibility = Visibility.Collapsed;

}, CancellationToken.None, TaskCreationOptions.None, uiScheduler).ContinueWith(task => Dispatcher.Invoke(() =>
{
    BusyIndicator.IsBusy = false;
}));

And using dispatcher:

BusyIndicator.IsBusy = true;

//long-running UI task
tableTreeViewItem.Visibility = Visibility.Collapsed;

BusyIndicator.IsBusy = false;

But it does not work, any ideas how to fix it?

PSS

I made some updates, I decided to grab all data and store which tree view item should be visible or hidden.

So I have class which stores table, visibility and visible columns for filter method

 class TreeViewItemVisibilityTableContainer
{
    private TreeViewItem _treeViewItem;
    private TableModel _table;
    private Visibility _visibility;
    private List<ColumnModel> _visibleColumns;

    public TableModel Table
    {
        get { return _table; }
        set { _table = value; }
    }
    public TreeViewItem TreeViewItem
    {
        get { return _treeViewItem; }
        set { _treeViewItem = value; }
    }
    public Visibility Visibility
    {
        get { return _visibility; }
        set { _visibility = value; }
    }
    public List<ColumnModel> VisibleColumns
    {
        get { return _visibleColumns; }
        set { _visibleColumns = value; }
    }
}

And now I can filter all this staff directly on UI thread:

 System.Action filterTreeViewItemsVisibility = () => Dispatcher.Invoke(() =>
            {
                foreach (var item in itemsToFilter)
                {
                    item.TreeViewItem.Visibility = item.Visibility;
                    var capturedItemForClosure = item;

                    if (item.Visibility == Visibility.Visible)
                    {
                        if (item.VisibleColumns.Any())
                        {
                            item.TreeViewItem.Items.Filter = item.TreeViewItem.Items.Filter =
                                treeViewItem =>
                                    capturedItemForClosure.VisibleColumns.Any(
                                        columnModel => columnModel.Equals(treeViewItem));
                        }
                        else
                        {
                            item.TreeViewItem.Visibility = Visibility.Collapsed;
                        }
                    }
                }
            });

            IoC.Get<IBusyIndicatorHelper>().PerformLongrunningAction(filterTreeViewItemsVisibility, IoC.Get<IShellViewModel>());

But it is still super slow

makambi
  • 1,100
  • 3
  • 13
  • 30
  • Your first snippet is no different from the second; you're running everything in the UI thread (using 3 different techniques) and running nothing in any non-UI thread. – Servy Dec 05 '14 at 16:54
  • I understand that, I just looking for a way to tell wpf to refresh binding. I expected Task to be created separately so original BusyIndicator.IsBusy = true; could complete – makambi Dec 05 '14 at 17:01
  • You'd still be blocking the UI thread immediately afterwards, preventing the busy indicator from being drawn. – Servy Dec 05 '14 at 17:03
  • Obviously, it's not the way to go. Filter *data items* in background, and give the controls required, filtered subset. – Dennis Dec 05 '14 at 17:03
  • "suppose I have long running task in code-behind which is performed on UI thread" -- that's the problem right there. If you insist on running that task on the UI thread, your UI won't be able to do anything else (like show the busy indication). Have you done any research at all regarding how to run tasks asynchronously, using `Task` or `BackgroundWorker`? If not, you should. If you have, you should explain what you've found and why you aren't doing that here. – Peter Duniho Dec 05 '14 at 18:02
  • I think you should work on making those two event more efficient. I am not getting why Visibility.Collapsed should take a long time. – paparazzo Dec 05 '14 at 18:37
  • @Blam, treeview with 1600 elements each of them can have 50, 100 or 200 nested items. It's quite time-consuming operation to set visibility on each of them, and this thing need to be performed on the ui thread. – makambi Dec 08 '14 at 10:05
  • Not arguing but I am I am just amazed that Collapsed takes time. Layout of a collapsed item seems like it should be fast. Do you have an idea of what is taking time? +1 – paparazzo Dec 08 '14 at 18:06
  • @Blam, collapsing is fast) Rendering with Visibility.Visible is super slow – makambi Dec 08 '14 at 18:36
  • Then you need to fix the question. "Lets say I iterate and set tableTreeViewItem.Visibility = Visibility.Collapsed; for each item." – paparazzo Dec 08 '14 at 18:40

1 Answers1

3

Here is my solution for the busy indicator.

There are few components to show the solutions

  • BusyIndicator User Control
  • AbortableBackgroundWorker
  • MainWindow as application window

BusyIndicator user control

BusyIndicator.xaml - pretty simply

<UserControl x:Class="BusyIndicatorExample.BusyInidicator"
         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" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300" Visibility="Collapsed">
<Grid Background="#BFFFFFFF" >
        <TextBlock Text="Loading data..." HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="#FF2C2C2C" FontSize="16" FontWeight="Bold" />
</Grid>
</UserControl>

BusyIndicator.xaml.cs

using System;
using System.Windows.Controls;

namespace BusyIndicatorExample
{
/// <summary>
/// Interaction logic for BusyInidcator.xaml
/// </summary>
public partial class BusyInidicator : UserControl
{
    public BusyInidicator()
    {
        InitializeComponent();
    }

Method for showing indicator

    public void Start()
    {
        this.Dispatcher.Invoke(new Action(delegate()
        {

            this.Visibility = System.Windows.Visibility.Visible;
        }), System.Windows.Threading.DispatcherPriority.Normal);

    }

Method for hiding indicator

    public void Stop()
    {
        this.Dispatcher.Invoke(new Action(delegate()
        {

            this.Visibility = System.Windows.Visibility.Collapsed;
        }), System.Windows.Threading.DispatcherPriority.Normal);
    }
}
}

AbortableBackgroundWorker for simulation non UI task

using System;
using System.ComponentModel;
using System.Threading;

namespace BusyIndicatorExample
{

/// <summary>
/// Abortable background worker
/// </summary>
public class AbortableBackgroundWorker : BackgroundWorker
{
    //Internal Thread
    private Thread workerThread;

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        try
        {
            base.OnDoWork(e);
        }
        catch (ThreadAbortException)
        {
            e.Cancel = true;        //We must set Cancel property to true! 
            Thread.ResetAbort();    //Prevents ThreadAbortException propagation 
        }
    }

    public void Abort()
    {
        if (workerThread != null)
        {
            workerThread.Abort();
            workerThread = null;
        }
    }
} 
}

Finally, MainWindow where the process is simulated

MainWindow.xaml

<Window x:Class="BusyIndicatorExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BusyIndicatorExample"
    Title="MainWindow" Height="350" Width="525">
<Grid>

    <Button Content="Start Data Loading" HorizontalAlignment="Left" Margin="63,42,0,0" VerticalAlignment="Top" Width="125" Height="28" Click="Button_Click"/>
   <TextBox HorizontalAlignment="Left" Height="23" Margin="63,87,0,0" TextWrapping="Wrap" Text="{Binding DataString}" VerticalAlignment="Top" Width="412"/>
    <local:BusyInidicator x:Name="busyIndicator" HorizontalAlignment="Left" Height="100" Margin="177,140,0,0" VerticalAlignment="Top" Width="300"/>
</Grid>
</Window>

MainWindow.xaml.cs - here is the application code

using System.ComponentModel;
using System.Windows;

namespace BusyIndicatorExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
    private AbortableBackgroundWorker _worker;

Constructor and public property binded to the textbox

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    private string _dataString = "No Data";

    public string DataString
    {
        get { return _dataString; }
        set {
            if (_dataString != value)
            {
                _dataString = value;
                if (PropertyChanged != null)
                    PropertyChanged.Invoke(this, new PropertyChangedEventArgs("DataString"));
            }
        }
    }

Button click event - initialize BackgroundWorker and starts it

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        if(_worker == null)
        {
            _worker = new AbortableBackgroundWorker();
            _worker.WorkerReportsProgress = true;
            _worker.WorkerSupportsCancellation = true;
            _worker.DoWork += _worker_DoWork;
            _worker.RunWorkerCompleted += _worker_RunWorkerCompleted;
        }

        if (!_worker.IsBusy)
            _worker.RunWorkerAsync();

    }

BackgroundWorker event handlers

RunWorkerCompleted update data string and hide indicator.

    void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        DataString = "Data has been loaded";
        busyIndicator.Stop();
    }

DoWork shows indicator and put to sleep thread for 5 sec.

    void _worker_DoWork(object sender, DoWorkEventArgs e)
    {
        DataString = "No Data";
        busyIndicator.Start();
            System.Threading.Thread.Sleep(5000);
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
}

Hope this helps. Modify code as you need to fit to your scenario Full example project code can be downloaded here

Miroslav Endyš
  • 154
  • 1
  • 8