5

This what i am trying to achieve with WPF. A textblock as title and below buttons in a wrappanel .The problem is that this needs scrolling etc. I have achieved this using ItemsControl and binding for each group. I have an ItemsControl that has a stackpanel as paneltemplate and its itemtemplate is a textblock and a wrappanel .

It works but it is slow at instantiation at slow intel gma + atom machines when items are many . It seems that rendering isnt the problem but creation of the Visual Tree. So my only bet here is to create a custom panel with virtualization i guess?

Here is what i have done. http://pastebin.com/u8C7ddP0
Above solution is slow at some machines.

I am looking for a solution that it would take max 100ms at slow machines to create. Thank you

UPDATE

 public class PreferenceCheckedConvertor : IMultiValueConverter
    {


    public object Convert(object[] values, Type targetType,
            object parameter, System.Globalization.CultureInfo culture)
    {

        var preference = values[0] as OrderItemPreference;
        var items = values[1] as ObservableCollection<OrderItemPreference>;

        var found = items.FirstOrDefault(item => item.Preference.Id == preference.Preference.Id);
        if (found == null)
        {
            return false;
        }
        return true;

    }
    public object[] ConvertBack(object value, Type[] targetTypes,
           object parameter, System.Globalization.CultureInfo culture)
    {
        try
        {
            return null;
        }
        catch (Exception e)
        {
            return null;
        }
    }


}

ff

public class PreferenceConvertor : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType,
                object parameter, System.Globalization.CultureInfo culture)
        {
            var preferences=values[0] as IEnumerable<Preference>;
            var items=values[1] as ObservableCollection<OrderItemPreference>;

            var newList = new List<OrderItemPreference>(preferences.Count());



            foreach (var preference in preferences)
            {
                var curItem = items.FirstOrDefault(item => item.Preference.Id == preference.Id);

                if (curItem == null)
                {
                    newList.Add(new OrderItemPreference()
                    {
                        Preference = preference
                    });
                }
                else
                {
                    newList.Add(curItem);
                }

            }

            return newList;







        }
        public object[] ConvertBack(object value, Type[] targetTypes,
               object parameter, System.Globalization.CultureInfo culture)
        {
            try
            {
                return null;
            }
            catch (Exception e)
            {
                return null;
            }
        }}

enter image description here

GorillaApe
  • 3,611
  • 10
  • 63
  • 106
  • There are a lot of attached properties in your code from namespaces which are never mentioned. This in particular looks suspicious: `cal:Message.Attach="[Event Checked]=[Action AddPreference($dataContext,false)]; [Event Unchecked]=[Action RemovePreference($datacontext,false)]"`. You never mention the nature and exact number of objects you bind to. All in all, there's too much code. See http://sscce.org/ Why do you think that virtualization is the bottleneck? Have you profiled your application? – Athari Jun 29 '13 at 06:40
  • this is event for caliburn micro. Actually there is no code it binds to a List. with properties you see. Items arent always many. – GorillaApe Jun 29 '13 at 09:43
  • Before asking how to improve performance, you need to profile your code. See [c# profiler](https://www.google.com/search?q=c%23+profiler). – Athari Jun 29 '13 at 10:56
  • @Athari i removed that cal:Message.Attach and see improvement. Ok you are telling me to profile something that doesnt have any c# code? Can it profile xaml speed ? – GorillaApe Jun 29 '13 at 11:50

1 Answers1

11

To make WPF layout faster, you need to enable virtualization. In your code:

  1. Remove ScrollViewer which wraps all your controls.
  2. Replace top-level ItemsControl with ListBox:

    <ListBox Name="items" HorizontalContentAlignment="Stretch"
             ScrollViewer.HorizontalScrollBarVisibility="Disabled" ... >
    
  3. Replace StackPanel in the ListBox's ItemsPanel with VirtualizingStackPanel:

    <VirtualizingStackPanel Orientation="Vertical" ScrollUnit="Pixel"
                            VirtualizationMode="Recycling"/>
    

This will enable virtualization for top-level items. On my computer, this allows to display 100,000 items within 1 second.

N.B.:

  1. While you think that the bottleneck is WPF layout, you may be wrong, as you haven't profiled your application. So while this answers your question, it may not actually solve the problem with the window working slow. Profilers can analyze not only your code, but framework code too. They analyze calls, memory etc., not your sources. They are a great tool to improve your performance and the only true way to find the source of performance issues.

  2. For the love of all that is holy, please, read http://sscce.org! You won't have enough reputation to give to solve all your code issues if you don't try to make your examples short, self-contained and compilable. Just to run your example, I had to create my own view-models, get rid of all irrelevant code, simplify bindings, not to mention all kinds of your own converters, controls and bindings which are nowhere described.

UPDATED to support .NET 4.0

public static class PixelBasedScrollingBehavior
{
    public static bool GetIsEnabled (DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled (DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior),
            new UIPropertyMetadata(false, IsEnabledChanged));

    private static void IsEnabledChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var isEnabled = (bool)e.NewValue;

        if (d is VirtualizingPanel) {
            if (TrySetScrollUnit(d, isEnabled))
                return;
            if (!TrySetIsPixelBased(d, isEnabled))
                throw new InvalidOperationException("Failed to set IsPixelBased or ScrollUnit property.");
        }
        if (d is ItemsControl) {
            TrySetScrollUnit(d, isEnabled);
        }
    }

    private static bool TrySetScrollUnit (DependencyObject ctl, bool isEnabled)
    {
        // .NET 4.5: ctl.SetValue(VirtualizingPanel.ScrollUnitProperty, isEnabled ? ScrollUnit.Pixel : ScrollUnit.Item);

        var propScrollUnit = typeof(VirtualizingPanel).GetField("ScrollUnitProperty", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
        if (propScrollUnit == null)
            return false;
        var dpScrollUnit = (DependencyProperty)propScrollUnit.GetValue(null);

        var assemblyPresentationFramework = typeof(Window).Assembly;
        var typeScrollUnit = assemblyPresentationFramework.GetType("System.Windows.Controls.ScrollUnit");
        if (typeScrollUnit == null)
            return false;
        var valueScrollUnit = Enum.Parse(typeScrollUnit, isEnabled ? "Pixel" : "Item");

        ctl.SetValue(dpScrollUnit, valueScrollUnit);
        return true;
    }

    private static bool TrySetIsPixelBased (DependencyObject ctl, bool isEnabled)
    {
        // .NET 4.0: ctl.IsPixelBased = isEnabled;

        var propIsPixelBased = ctl.GetType().GetProperty("IsPixelBased", BindingFlags.NonPublic | BindingFlags.Instance);
        if (propIsPixelBased == null)
            return false;

        propIsPixelBased.SetValue(ctl, isEnabled, null);
        return true;
    }
}

It is necessary to set local:PixelBasedScrollingBehavior.IsEnabled="True" both on ListBox and VirtualizingStackPanel, otherwise scrolling will work in item mode. The code compiles in .NET 4.0. If .NET 4.5 is installed, it will use new properties.

Working example:

MainWindow.xaml

<Window x:Class="So17371439ItemsLayoutBounty.MainWindow" x:Name="root"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:So17371439ItemsLayoutBounty"
        Title="MainWindow">

    <Window.Resources>
        <Style x:Key="OrderRadioButton" TargetType="{x:Type RadioButton}"></Style>
        <Style x:Key="OrderCheckboxButton" TargetType="{x:Type ToggleButton}"></Style>
        <Style x:Key="OrderProductButton" TargetType="{x:Type Button}"></Style>
    </Window.Resources>

    <ListBox Name="items" ItemsSource="{Binding PreferenceGroups, ElementName=root}" HorizontalContentAlignment="Stretch" ScrollViewer.HorizontalScrollBarVisibility="Disabled" local:PixelBasedScrollingBehavior.IsEnabled="True">
        <ItemsControl.Resources>
            <ItemsPanelTemplate x:Key="wrapPanel">
                <WrapPanel/>
            </ItemsPanelTemplate>

            <DataTemplate x:Key="SoloSelection" DataType="local:PreferenceGroup">
                <ItemsControl ItemsSource="{Binding Preferences}" ItemsPanel="{StaticResource wrapPanel}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <RadioButton Width="146" Height="58" Margin="0,0,4,4" GroupName="{Binding GroupId}" Style="{StaticResource OrderRadioButton}">
                                <TextBlock Margin="4,0,3,0" VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Name}"/>
                            </RadioButton>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </DataTemplate>

            <DataTemplate x:Key="MultiSelection" DataType="local:PreferenceGroup">
                <ItemsControl ItemsSource="{Binding Preferences}" ItemsPanel="{StaticResource wrapPanel}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <ToggleButton Width="146" Height="58" Margin="0,0,4,4" Style="{StaticResource OrderCheckboxButton}">
                                <TextBlock Margin="4,0,3,0" VerticalAlignment="Center" TextWrapping="Wrap" Text="{Binding Name}"/>
                            </ToggleButton>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </DataTemplate>

            <DataTemplate x:Key="MultiQuantitySelection" DataType="local:PreferenceGroup">
                <ItemsControl ItemsSource="{Binding Preferences}" ItemsPanel="{StaticResource wrapPanel}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Grid Width="146" Height="58" Margin="0,0,4,4">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Button Name="quantity" Background="White" Width="45" Style="{StaticResource OrderProductButton}">
                                    <TextBlock Text="{Binding Quantity}"/>
                                </Button>
                                <Button Margin="-1,0,0,0" Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Style="{StaticResource OrderProductButton}">
                                    <TextBlock TextWrapping="Wrap" HorizontalAlignment="Left" TextTrimming="CharacterEllipsis" Text="{Binding Name}"/>
                                </Button>
                            </Grid>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </DataTemplate>

        </ItemsControl.Resources>

        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                    <TextBlock FontSize="25" FontWeight="Light" Margin="0,8,0,5" Text="{Binding Name}"/>
                    <ContentControl Content="{Binding}" Name="items"/>
                </StackPanel>

                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding SelectionMode}" Value="1">
                        <Setter TargetName="items" Property="ContentTemplate" Value="{StaticResource SoloSelection}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding SelectionMode}" Value="2">
                        <Setter TargetName="items" Property="ContentTemplate" Value="{StaticResource MultiSelection}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding SelectionMode}" Value="3">
                        <Setter TargetName="items" Property="ContentTemplate" Value="{StaticResource MultiQuantitySelection}"/>
                    </DataTrigger>
                </DataTemplate.Triggers>

            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel x:Name="panel" Orientation="Vertical" VirtualizationMode="Recycling" local:PixelBasedScrollingBehavior.IsEnabled="True"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

    </ListBox>

</Window>

MainWindow.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;

namespace So17371439ItemsLayoutBounty
{
    public partial class MainWindow
    {
        public ObservableCollection<PreferenceGroup> PreferenceGroups { get; private set; }

        public MainWindow ()
        {
            var rnd = new Random();
            PreferenceGroups = new ObservableCollection<PreferenceGroup>();
            for (int i = 0; i < 100000; i++) {
                var group = new PreferenceGroup { Name = string.Format("Group {0}", i), SelectionMode = rnd.Next(1, 4) };
                int nprefs = rnd.Next(5, 40);
                for (int j = 0; j < nprefs; j++)
                    group.Preferences.Add(new Preference { Name = string.Format("Pref {0}", j), Quantity = rnd.Next(100) });
                PreferenceGroups.Add(group);
            }
            InitializeComponent();
        }
    }

    public class PreferenceGroup
    {
        public string Name { get; set; }
        public int SelectionMode { get; set; }
        public ObservableCollection<Preference> Preferences { get; private set; }

        public PreferenceGroup ()
        {
            Preferences = new ObservableCollection<Preference>();
        }
    }

    public class Preference
    {
        public string Name { get; set; }
        public string GroupId { get; set; }
        public int Quantity { get; set; }
    }

    public static class PixelBasedScrollingBehavior
    {
        public static bool GetIsEnabled (DependencyObject obj)
        {
            return (bool)obj.GetValue(IsEnabledProperty);
        }

        public static void SetIsEnabled (DependencyObject obj, bool value)
        {
            obj.SetValue(IsEnabledProperty, value);
        }

        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior),
                new UIPropertyMetadata(false, IsEnabledChanged));

        private static void IsEnabledChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var isEnabled = (bool)e.NewValue;

            if (d is VirtualizingPanel) {
                if (TrySetScrollUnit(d, isEnabled))
                    return;
                if (!TrySetIsPixelBased(d, isEnabled))
                    throw new InvalidOperationException("Failed to set IsPixelBased or ScrollUnit property.");
            }
            if (d is ItemsControl) {
                TrySetScrollUnit(d, isEnabled);
            }
        }

        private static bool TrySetScrollUnit (DependencyObject ctl, bool isEnabled)
        {
            // .NET 4.5: ctl.SetValue(VirtualizingPanel.ScrollUnitProperty, isEnabled ? ScrollUnit.Pixel : ScrollUnit.Item);

            var propScrollUnit = typeof(VirtualizingPanel).GetField("ScrollUnitProperty", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
            if (propScrollUnit == null)
                return false;
            var dpScrollUnit = (DependencyProperty)propScrollUnit.GetValue(null);

            var assemblyPresentationFramework = typeof(Window).Assembly;
            var typeScrollUnit = assemblyPresentationFramework.GetType("System.Windows.Controls.ScrollUnit");
            if (typeScrollUnit == null)
                return false;
            var valueScrollUnit = Enum.Parse(typeScrollUnit, isEnabled ? "Pixel" : "Item");

            ctl.SetValue(dpScrollUnit, valueScrollUnit);
            return true;
        }

        private static bool TrySetIsPixelBased (DependencyObject ctl, bool isEnabled)
        {
            // .NET 4.0: ctl.IsPixelBased = isEnabled;

            var propIsPixelBased = ctl.GetType().GetProperty("IsPixelBased", BindingFlags.NonPublic | BindingFlags.Instance);
            if (propIsPixelBased == null)
                return false;

            propIsPixelBased.SetValue(ctl, isEnabled, null);
            return true;
        }
    }
}
Athari
  • 33,702
  • 16
  • 105
  • 146
  • ok but this is impactical as sometimes buttons dont fit the screen(like one preferencegroup with many preferences).So without virtualizing wrappanel it would jump. After removing that attached property and doing some tweeks and fixing a timer that was firing i got better performance. However the lag is at some platforms. I have problem with ATOM and GMA GPU. But even with a Pentium 4 and an old matrox g450 card got no lag.. – GorillaApe Jul 02 '13 at 23:34
  • What exactly do you mean by "impractical"? If there is any difference in behavior between your example and mine, describe it. Remember, I can't run your example (see 2nd N.B.). What "that" attached property? What timer? Oh come on, do you really expect me to read your mind? I can't help you if you don't provide ANY information. You haven't said even what number of items you need to display. – Athari Jul 02 '13 at 23:39
  • Your example was very close to mine. I mean 90% except the styling. To run my example it would be really difficult as i would have to include many files.However i will update with the convertor. By attached property i mean what you told me at your first comment at my question. Number of items vary .I mean some users have from 5 to 100. Also because i didnt mention i am not using WPF 4.5 because i cant for supporting WIN XP so there isnt pixel scrolling :(.Tried but didnt work like mentioned here http://stackoverflow.com/questions/14349848/wpf-4-0-pixel-based-scrolling-in-virtualizingstackpanel – GorillaApe Jul 02 '13 at 23:57
  • I don't see attached properties related to pixel-scrolling in your code. If it doesn't work, provide the code. – Athari Jul 03 '13 at 00:10
  • You used ScrollUnit="Pixel" I used this behavior http://stackoverflow.com/a/9875475/294022 and replaced ScrollUnit="Pixel" with what is at this answer. However didnt work – GorillaApe Jul 03 '13 at 00:13
  • NET 4 . Mine OS is Windows 7x64 bit on amd 5850 gpu intel i7 930. However i have deploy it at multiple computers and only intel gma with atom had some lag with Windows 7 64bit.Here is a simulation how it feels on touch http://jsfiddle.net/4MVvN/61/ ( a bit better now ) – GorillaApe Jul 03 '13 at 00:36
  • Are you sure you have .NET 4 and not .NET 4.5 on Windows 7? When .NET 4.5 is installed (and it is installed automatically unless you have disabled Windows Update), it replaces .NET 4.0. Try adding support for `ScrollUnit` property, like suggested in the discussion of the question you linked to. – Athari Jul 03 '13 at 00:48
  • i have installed 4.5 too. I dont have that much knowledge to do that now .It seems that this property should be set somewhere else.I'll try though. Thanks for helping.I noticed that my IMultiValueConverter contributed to the lag i was experiencing.Not the converter its self but the code – GorillaApe Jul 03 '13 at 09:30
  • Updated the code to support virtualization in pixel mode for .NET 4.0 on Windows XP, .NET 4.0 and .NET 4.5 on Windows 7. Target framerwork is .NET 4.0, but the code uses new features if .NET 4.5 is installed. – Athari Jul 06 '13 at 08:59
  • also which profiler should I use ? I noticed a little delay after leaving the application open for a day idle but no memory etc increased. – GorillaApe Jul 06 '13 at 12:36
  • I'm using tools from [JetBrains](http://www.jetbrains.com/products.html#dotnet), but there're many other profilers. – Athari Jul 06 '13 at 12:41