4

I'm creating some UI programmatically which involves some heavy processing. Basically what i want is to run a loader animation while my UI is being built/added to the window. The UI being added are some Grids and some images being loaded into them.

So far i've tried BackgroundWorker but since i need to use the UI thread to add the UI i'm building, the loader won't start/animate until the UI being added is finished.

I've also tried some async methods without any results. My last attempt was something like this, but it still need to access the UI thread which would eventually freeze the UI animation until the work is done

   private async void begin()
    {

        await this.LongRunOpAsync();

    }

    public Task LongRunOpAsync()
    {
        return Task.Run(() =>
        {
            InitGrid();
            stopLoadingScreen();
        });
    }

I might be missing something, but i don't know what and i'm also out of ideas to do this. Any help would be appreciated.

EDIT: Method that has the heavy work

private void makeIgrid()
        {
            Grid hostgrid = new Grid();
            hostgrid.Name = "imagesHostGrid";
            hostgrid.Width = 700;
            hostgrid.VerticalAlignment = VerticalAlignment.Top;
            hostgrid.HorizontalAlignment = HorizontalAlignment.Center;
            hostgrid.SetValue(Canvas.ZIndexProperty, 0);
            this.RegisterName(hostgrid.Name, hostgrid);

            Grid imagegrid = new Grid();
            imagegrid.Name = "imagegrid";
            imagegrid.Height = height2;
            //imagegrid.Width = 700;
            imagegrid.SetValue(Canvas.ZIndexProperty, 0);
            imagegrid.VerticalAlignment = VerticalAlignment.Top;
            imagegrid.HorizontalAlignment = HorizontalAlignment.Center;
            imagegrid.Margin = new Thickness(0, height1, 0, 0);//(left,top,right,bottom)

            RowDefinition iRow1 = new RowDefinition();
            iRow1.Height = new GridLength(2, GridUnitType.Star);
            imagegrid.RowDefinitions.Add(iRow1);

            RowDefinition iRow2 = new RowDefinition();
            iRow2.Height = new GridLength(70, GridUnitType.Star);
            imagegrid.RowDefinitions.Add(iRow2);

            ScrollViewer sv = new ScrollViewer
            {
                CanContentScroll = true,
                HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
                VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
            };

            for (int i = 0; i < images.Length; i++)
            {
                ColumnDefinition columns = new ColumnDefinition();
                columns.MinWidth = 100;
                columns.Width = new GridLength(100, GridUnitType.Star);
                imagegrid.ColumnDefinitions.Add(columns);

                BitmapImage bmp = new BitmapImage();
                bmp.BeginInit();
                bmp.UriSource = new Uri(currentDirectory + "//Media//Images//" + selectedFolder + "//" + System.IO.Path.GetFileName(images[i].ToString()), UriKind.Relative);
                bmp.CacheOption = BitmapCacheOption.OnLoad;
                Debug.WriteLine("Loading: " + currentDirectory + "//Media//Images//" + selectedFolder + "//" + System.IO.Path.GetFileName(images[i].ToString()));
                bmp.EndInit();

                Image img = new Image();

                img.Name = System.IO.Path.GetFileNameWithoutExtension(images[i].ToString());
                img.Source = bmp;
                img.VerticalAlignment = VerticalAlignment.Center;
                img.HorizontalAlignment = HorizontalAlignment.Center;
                img.TouchDown += addImagetoScreen;
                img.Width = 94;
                img.Stretch = Stretch.Uniform;
                img.SetValue(Canvas.ZIndexProperty, 0);
                this.RegisterName(img.Name, img);

                Border border = new Border();
                border.SetResourceReference(Control.BackgroundProperty, "MenuSelected");
                border.SetValue(Canvas.ZIndexProperty, 0);

                Grid.SetRow(border, 0);
                Grid.SetColumn(border, i);

                Grid.SetRow(img, 1);
                Grid.SetColumn(img, i);

                imagegrid.Children.Add(border);
                imagegrid.Children.Add(img);

            }
            sv.Content = imagegrid;
            sv.SetValue(Canvas.ZIndexProperty, 0);
            hostgrid.Children.Add(sv);
            mainGrid.Children.Add(hostgrid);



        }
user3091405
  • 53
  • 1
  • 5

2 Answers2

17

Ok. Delete all your code and start all over.

This is how you do that in WPF:

<Window x:Class="WpfApplication14.ItemsControlSample2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ItemsControlSample2" WindowState="Maximized">
    <ItemsControl ItemsSource="{Binding}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border Background="DarkGray" BorderBrush="Black" BorderThickness="1" CornerRadius="5"
                        Width="100" Height="100" Margin="10" >
                    <Grid>
                        <Image x:Name="img" Source="{Binding ImageSource}" Margin="2"/>

                        <TextBlock x:Name="txt" Text="Loading..." FontWeight="Bold"
                               VerticalAlignment="Center" HorizontalAlignment="Center"
                               Visibility="Collapsed" Foreground="AliceBlue"/>
                    </Grid>
                </Border>

                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding IsLoading}" Value="True">
                        <Setter TargetName="img" Property="Source" Value="{x:Null}"/>
                        <Setter TargetName="txt" Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </ItemsControl.ItemTemplate>

        <ItemsControl.Template>
            <ControlTemplate TargetType="ItemsControl">
                <ScrollViewer>
                    <WrapPanel IsItemsHost="True"/>
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
    </ItemsControl>
</Window>

Code Behind:

public partial class ItemsControlSample2 : Window
{
    public ItemsControlSample2()
    {
        InitializeComponent();

        //Make sure you change this path to a valid path in your PC where you have JPG files
        var path = @"F:\Media\Images\My Drums";

        var images = Directory.GetFiles(path,"*.jpg")
                              .Select(x => new ImageViewModel()
                                           {
                                               Path = x,
                                           });
        DataContext = images.ToList();
    }
}

Data Item:

public class ImageViewModel : INotifyPropertyChanged
{
    private bool _isLoading;
    public bool IsLoading
    {
        get { return _isLoading; }
        set
        {
            _isLoading = value;
            OnPropertyChanged("IsLoading");
        }
    }

    private ImageSource _imageSource;
    public ImageSource ImageSource
    {
        get { return _imageSource; }
        set
        {
            _imageSource = value;
            OnPropertyChanged("ImageSource");
        }
    }

    private string _path;
    public string Path
    {
        get { return _path; }
        set
        {
            _path = value;
            OnPropertyChanged("Path");

            LoadImageAsync();
        }
    }

    private void LoadImageAsync()
    {
        IsLoading = true;

        var UIScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        Task.Factory.StartNew(() =>
        {
            var bmp = new BitmapImage();
            bmp.BeginInit();
            bmp.UriSource = new Uri(Path, UriKind.Relative);
            bmp.CacheOption = BitmapCacheOption.OnLoad;
            bmp.EndInit();
            bmp.Freeze();
            return bmp;
        }).ContinueWith(x =>
        {
            ImageSource = x.Result;
            IsLoading = false;
        },UIScheduler);
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

Result:

enter image description here

  • Notice how I'm declaratively defining the UI in XAML as opposed to procedurally creating it in C# code. This is a much cleaner approach because it lets WPF do it's job

  • I'm using an ItemsControl which is the appropiate approach for all "items"-based UIs in WPF. Regardless of their visual appearance.

  • Also notice how I'm leveraging DataBinding in order to populate the UI with actual data and also DataTriggers in order to create a basic stateful behavior.

    • While the image is loading (IsLoading == true), the Image.Source is null and the TextBlock is shown.
    • When the image finished loading (IsLoading == false), the Image.Source is bound to the ViewModel data and the TextBlock is hidden.
  • See how I'm using a WrapPanel for layout instead of manually placing the items. This gives you an "Explorer-like" behavior. Try resizing the Window to see the results.

  • I strongly suggest you read the above linked documentation, mostly the ItemsControl stuff and also Rachel's WPF Mentality post.

  • WPF Rocks. Just copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.

  • I'm Using C# 4.0 and .Net 4.0, so I don't have async/await. You should be able to remove all the Task based code and replace that by this newer, cleaner async approach.

  • Let me know if you need further help.

Community
  • 1
  • 1
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • Thanks for the help. I'm still new at WPF so i'm still learning allot. As you said WPF Rocks , its incredibly easy to understand and allow us to do really nice things. What I was trying to do isn't possible the way i was going , so I mark your reply as the answer , since you explained everything very well. Thanks to everyone else that helped too :). I'll probably ask all of you for help soon hehe – user3091405 Jan 08 '14 at 21:54
  • 2
    Add `bmp.DecodePixelWidth = 94;` after `bmp.BeginInit();` to save memory and to speed up the display – Maxence May 28 '14 at 18:09
  • hi, i am getting error freezable object cannot be freezed any idea ? please – sm.abdullah Oct 27 '16 at 14:59
  • in my case URI is https absolute URL. if i use Filepath then it works ? any idea. – sm.abdullah Oct 27 '16 at 16:11
3

I don't see any heavy work there, aside from maybe loading images.

Ideally, you should create all of the UI elements (border/grid/etc) on the ui thread, and only do the image loading in another thread.

Or better yet, instead of creating all that UI programmatically, you should be using some form of ItemsControl and DataTemplate to generate all the ui, and load all of your images async in some kind of view model like thing.

John Gardner
  • 24,225
  • 5
  • 58
  • 76
  • +1, though basically repeating what I just said in the comments. – Federico Berasategui Jan 08 '14 at 17:25
  • But since "the heavy work" is loading the images, using the ItemsControl wouldn't reproduce the same problem which is freezing the UI ? The images that are being loaded won't always be the same as the number of images won't be too, its all Dynamic. What makes it better to use an ItemsControl? Will allow me to have a loading screen being shown and the images being added to the ItemsControl at same time ? thanks for all the tips btw – user3091405 Jan 08 '14 at 17:36
  • @HighCore yup, I posted my answer just as you posted your comment. Great minds think alike! – John Gardner Jan 08 '14 at 18:14
  • @user3091405 Ideally, you'd have some kind of "placeholder" image that you show until the background loading completes. even windows explorer does this if you scroll super fast, it doesn't waste time loading images you're just going to scroll past. So if you need to load 100 images, you put the 100 items in your data model, show a placeholder, and then once the real data comes back async, you fire property change notifications, and let data binding make things update – John Gardner Jan 08 '14 at 18:16