1

Code sample:

XAML

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

    <testWpfApp:GifImage x:Name="gifImage" Stretch="None" GifSource="/loading.gif" AutoStart="True" Width="50" Height="50" Margin="0,0,300,0"/>
    <ComboBox Width="200" Height="40" Name="Cb"></ComboBox>
</Grid>
</Window>

where GifImage is class from high rated answer from How do I get an animated gif to work in WPF?

C# code-behind

public partial class MainWindow : Window
{


    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 14000; i++)
        {
            Cb.Items.Add("Item number" + i);
        }


    }
}

When I make first click on combobox I get GUI freeze on 5-7 seconds and I want to show animated gif when GUI is freeze. How can I make it?

halfer
  • 19,824
  • 17
  • 99
  • 186
Frank59
  • 3,141
  • 4
  • 31
  • 53
  • 2
    1 - There are better ways to show animations than animated GIF in WPF. 2 - There are better ways to prevent the UI from freezing, such as doing heavy tasks in background threads. – Federico Berasategui Feb 21 '13 at 19:13
  • My heavy task - is GUI drawing. And i want to show users that my app is working when it makes. – Frank59 Feb 21 '13 at 19:27
  • can I ask what kind of GUI drawing?, anyways, the solution by Willem seems fine, which is basically the only one you can do if you're doing heavy stuff in the UI thread. – Federico Berasategui Feb 21 '13 at 19:37
  • Is drawing Telerik ComboBox with near 3000 elements. Will try Willem solution. – Frank59 Feb 21 '13 at 19:48
  • sorry but the items of the ComboBox should be Data Items, and creating that is a responsibibility of the ViewModel, not the View. If you're manually adding the items as in the above example, I strongly suggest you reconsider your approach. Also, A ComboBox with more than say 15 or 20 items is a bad idea from a UX perspective, unless it's some kind of Lookup Editor – Federico Berasategui Feb 21 '13 at 20:04
  • 1
    I also want to mention that there's no such thing as "Drawing" a ComboBox. You must leave the winforms mindset behind if you are working with WPF. A Clear separation must exist between data/behavior and presentation, and you'll never have these kind of problems. – Federico Berasategui Feb 21 '13 at 20:05
  • Post your current code and maybe we can work on improving it – Federico Berasategui Feb 21 '13 at 20:06
  • thanks for help. I undestand that 3k items is too much for Combo, but it's old big project with strange architecture. Now i really haven't time refactor it. – Frank59 Feb 21 '13 at 20:18

2 Answers2

4

You want something like this:

First VisualWrapper class:

[ContentProperty("Child")]
    public class VisualWrapper : FrameworkElement
    {
        public Visual Child
        {
            get
            {
                return _child;
            }

            set
            {
                if (_child != null)
                {
                    RemoveVisualChild(_child);
                }

                _child = value;

                if (_child != null)
                {
                    AddVisualChild(_child);
                }
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (_child != null && index == 0)
            {
                return _child;
            }
            else
            {
                throw new ArgumentOutOfRangeException("index");
            }
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return _child != null ? 1 : 0;
            }
        }

        private Visual _child;
    }

And then VisualTargetPresentationSource:

public class VisualTargetPresentationSource : PresentationSource
    {
        public VisualTargetPresentationSource(HostVisual hostVisual)
        {
            _visualTarget = new VisualTarget(hostVisual);
        }

        public override Visual RootVisual
        {
            get
            {
                return _visualTarget.RootVisual;
            }

            set
            {
                Visual oldRoot = _visualTarget.RootVisual;

                // Set the root visual of the VisualTarget.  This visual will
                // now be used to visually compose the scene.
                _visualTarget.RootVisual = value;

                // Tell the PresentationSource that the root visual has
                // changed.  This kicks off a bunch of stuff like the
                // Loaded event.
                RootChanged(oldRoot, value);

                // Kickoff layout...
                UIElement rootElement = value as UIElement;
                if (rootElement != null)
                {
                    rootElement.Measure(new Size(Double.PositiveInfinity,
                                                 Double.PositiveInfinity));
                    rootElement.Arrange(new Rect(rootElement.DesiredSize));
                }
            }
        }

        protected override CompositionTarget GetCompositionTargetCore()
        {
            return _visualTarget;
        }

        public override bool IsDisposed
        {
            get
            {
                // We don't support disposing this object.
                return false;
            }
        }

        private VisualTarget _visualTarget;
    }

This in your XAML:

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

    <testWpfApp:GifImage x:Name="gifImage" Stretch="None" GifSource="/loading.gif" AutoStart="True" Width="50" Height="50" Margin="0,0,300,0"/>
    <ComboBox Width="200" Height="40" Name="Cb"></ComboBox>

<vw:VisualWrapper Height="160" Width="160" VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="visualWrapper" />
</Grid>
</Window>

Then LoadingPanel class:

public class LoadingPanel
    {
        private VisualWrapper.VisualWrapper visualWrapper;

        public LoadingPanel(VisualWrapper.VisualWrapper _visualWrapper)
        {
            visualWrapper = _visualWrapper;
        }

        public VisualWrapper.VisualWrapper VisualWrapper
        {
            get { return visualWrapper; }
            set { visualWrapper = value; }
        }

        #region WaitDailog

        public HostVisual CreateMediaElementOnWorkerThread()
        {
            // Create the HostVisual that will "contain" the VisualTarget
            // on the worker thread.
            HostVisual hostVisual = new HostVisual();

            // Spin up a worker thread, and pass it the HostVisual that it
            // should be part of.

            Thread thread = new Thread(new ParameterizedThreadStart(MediaWorkerThread));
            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start(new object[] { hostVisual, visualWrapper });

            // Wait for the worker thread to spin up and create the VisualTarget.
            s_event.WaitOne();

            return hostVisual;
        }

        private FrameworkElement CreateMediaElement(VisualWrapper visualWrapper)
        {
            BitmapImage bi = new BitmapImage(new Uri(--YOURIMAGEPATH--));//Image path goes here
            Image image = new Image();
            image.Source = bi;
            image.Height = 150;
            image.Width = 150;
            //image.Margin = new Thickness(-150, -150, -150, -150);

            ImageBehavior.SetAnimatedSource(image, bi);//See http://wpfanimatedgif.codeplex.com/

            BrushConverter conv = new BrushConverter();
            //SolidColorBrush brush = conv.ConvertFromString("#6C8BBA") as SolidColorBrush;
            Border border = new Border();
            border.Background = Brushes.Transparent;
            //border.BorderBrush = brush;
            //border.BorderThickness = new Thickness(3);
            //border.Margin = new Thickness(-85, -140, 0, 0);

            border.Child = image;

            return border;
        }

        private void MediaWorkerThread(object arg)
        {
            // Create the VisualTargetPresentationSource and then signal the
            // calling thread, so that it can continue without waiting for us.
            HostVisual hostVisual = (HostVisual)((object[])arg)[0];
            VisualWrapper visualWrapper = (VisualWrapper)((object[])arg)[1];

            VisualTargetPresentationSource visualTargetPS = new VisualTargetPresentationSource(hostVisual);
            s_event.Set();

            // Create a MediaElement and use it as the root visual for the
            // VisualTarget.
            visualTargetPS.RootVisual = CreateMediaElement(visualWrapper);

            // Run a dispatcher for this worker thread.  This is the central
            // processing loop for WPF.
            System.Windows.Threading.Dispatcher.Run();
        }

        private static AutoResetEvent s_event = new AutoResetEvent(false);

        public bool ShowWaitDialog()
        {
            if (visualWrapper != null)
            {
                if (visualWrapper.Child == null)
                {
                    visualWrapper.Child = CreateMediaElementOnWorkerThread();
                }
            }

            return true;
        }

        public bool DisposeWaitDialog()
        {
            if (visualWrapper != null)
            {
                visualWrapper.Child = null;
            }

            return true;
        }

        #endregion

    }

The in code:

public MainWindow()
{
    InitializeComponent();

    VisualWrapper visualWrapper = (VisualWrapper)this.FindName("visualWrapper");

    LoadingPanel loadingPanel = new LoadingPanel(visualWrapper);

    Dispatcher.Invoke((Action)(() =>
    {
        loadingPanel.ShowWaitDialog();
    }), DispatcherPriority.Send, null);

     Task.Factory.StartNew(() =>
    {
        List<string> list = new List<string>();

        for (int i = 0; i < 14000; i++)
        {
            list.Add("Item number" + i);
        }

        Dispatcher.BeginInvoke((Action)(() =>
        {
            Cb.ItemsSource = list;
        }), DispatcherPriority.Normal, null);
    }, TaskCreationOptions.LongRunning);
}

Edit VirtualizingStackPanel:

An easy way to implement this is to create an ItemsPanelTemplate as a Resource and reference it in the ComboBox markup.  

  <Window.Resources>
    <ItemsPanelTemplate x:Key="VSP">
      <VirtualizingStackPanel/>
    </ItemsPanelTemplate>
  </Window.Resources>


    <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"
             VerticalAlignment="Top"
             ItemsSource="{Binding}"
             ItemsPanel="{StaticResource VSP}"
             ScrollViewer.IsDeferredScrollingEnabled="True">
    </ComboBox>  
Specifically, the ItemsPanel property of the ComboBox is set to that ItemsPanelTemplate Resource.

If you prefer, you can include the VirtualizingStackPanel right in the ComboBox creation markup: 

   <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"
             VerticalAlignment="Top"
             ItemsSource="{Binding}"
             ScrollViewer.IsDeferredScrollingEnabled="True">
      <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
          <VirtualizingStackPanel />
        </ItemsPanelTemplate>
      </ComboBox.ItemsPanel>
    </ComboBox> 
Willem
  • 9,166
  • 17
  • 68
  • 92
  • Sorry, can you tell what References i need to add for using ImageBehavior and VisualTargetPresentationSource? – Frank59 Feb 21 '13 at 19:53
  • [ImageBehavior](http://wpfanimatedgif.codeplex.com/) is an external dll. I missed the other class. See my updates – Willem Feb 21 '13 at 19:57
  • Thanks. Now it compile. But I getting "The calling thread cannot access this object because a different thread owns it" on row Cb.Items.Add("Item number" + i); And some fix: If i undestand correctly than you start fill comboitems in other process but in my variant it not raise GUI freez. GUI freez happens when WPF drawing comboitems on first combobox dropdown – Frank59 Feb 21 '13 at 20:03
  • What you need to look into is [VirtualizingStackPanel](http://www.codeproject.com/Articles/20616/WPF-Databound-ComboBox-Performance) and `ScrollViewer.IsDeferredScrollingEnabled="True"` – Willem Feb 22 '13 at 08:06
-2

Your problem is you are creating a ComboBox with 14000 items. That will take a long time to create, and will be unusable after you create it. Do you really expect your users to select an item from a list of 14000?

Rethink your UI. If you really need to have users select from a list of 14000 possibilities, consider a search text field with typeahead.

If you absolutely must have a ComboBox with thousands of items, look into control virtualization.

Community
  • 1
  • 1
Dour High Arch
  • 21,513
  • 29
  • 75
  • 90
  • I creating 14000 comboItems only for sample. I make this simple example for show my GUI freeze problem. In my app i have GUI freeze on 5-10 sec when my UserControl is draw youself and i want to show 'loading' animation for that time. – Frank59 Feb 21 '13 at 19:22