Yeshurun Kubi
Yeshurun Kubi

Reputation: 136

WPF - UserControl constructing performance (very poor)

I have a more complex code on my hand, but to ask this question I am bringing a simpler example of code.

My App is going to iterate throughout all glyphs in a specific font (expected 500 to 5000 glyphs). Each glyph should have a certain custom visual, and some functionality in it. For that I thought that best way to achieve that is to create a UserControl for each glyph.

On the checking I have made, as my UserControl gets more complicated, it takes more time to construct it. Even a simple adding of Style makes a meaningful effect on the performance.

What I have tried in this example is to show in a ListBox 2000 glyphs. To notice the performance difference I put 2 ListBoxes - First is binding to a simple ObservableCollection of string. Second is binding to ObservableCollection of my UserControl.

This is my MainWindow xaml:

    <Grid Background="WhiteSmoke">
      <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <ListBox Margin="10" ItemsSource="{Binding MyCollection}"></ListBox>
      <ListBox Margin="10" Grid.Row="1" ItemsSource="{Binding UCCollection}"
             VirtualizingPanel.IsVirtualizing="True"
             VirtualizingPanel.VirtualizationMode="Recycling"></ListBox>
    </Grid>

On code behind I have 2 ObservableCollection as mentioned:

public static ObservableCollection<string> MyCollection { get; set; } = new ObservableCollection<string>();
public static ObservableCollection<MyUserControl> UCCollection { get; set; } = new ObservableCollection<MyUserControl>();

For the first List of string I am adding like this:

    for (int i = 0; i < 2000; i++)
    {
       string glyph = ((char)(i + 33)).ToString();
       string hex = "U+" + i.ToString("X4");
       MyCollection.Add($"Index {i}, Hex {hex}:  {glyph}");
    }

For the second List of MyUserControl I am adding like this:

    for (int i = 0; i < 2000; i++)
    {
       UCCollection.Add(new MyUserControl(i + 33));
    }

MyUserControl xaml looks like this:

<Border Background="Black" BorderBrush="Orange" BorderThickness="2" MinWidth="80" MinHeight="80">
    <Grid Margin="5">
      <Grid.RowDefinitions>
        <RowDefinition Height="2*"></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <TextBlock HorizontalAlignment="Center" Foreground="White" FontSize="40" Text="{Binding Glyph}"/>
      <TextBlock HorizontalAlignment="Center" Foreground="OrangeRed" Text="{Binding Index}" Grid.Row="1"/>
      <TextBlock HorizontalAlignment="Center" Foreground="White" Text="{Binding Hex}" Grid.Row="2"/>
    </Grid>
  </Border>

And code behind of MyUserControl:

    public partial class MyUserControl : UserControl
    {
        private int OrgIndex { get; set; } = 0;
        public string Hex => "U+" + OrgIndex.ToString("X4");
        public string Index => OrgIndex.ToString();
        public string Glyph => ((char)OrgIndex).ToString();

        public MyUserControl(int index)
        {
            InitializeComponent();
            OrgIndex = index;
        }
    }

In order to follow the performance issue I have used Stopwatch. Adding 2000 string items to the first list took 1ms. Adding 2000 UserControls to the second list took ~1100ms. And it is just a simple UserControl, when I add some stuff to it, it takes more time and performance getting poorer. For example if I just add this Style to Border time goes up to ~1900ms:

     <Style TargetType="{x:Type Border}" x:Key="BorderMouseOver">
      <Setter Property="Background" Value="Black" />
      <Setter Property="BorderBrush" Value="Orange"/>
      <Setter Property="MinWidth" Value="80"/>
      <Setter Property="MinHeight" Value="80"/>
      <Setter Property="BorderThickness" Value="2" />
      <Style.Triggers>
        <DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}" Value="True">
          <Setter Property="Background" Value="#FF2A3137" />
          <Setter Property="BorderBrush" Value="#FF739922"></Setter>
        </DataTrigger>
      </Style.Triggers>
    </Style>

I am not fully familiar with WPF work around, so I will really appreciate your help. Is this a totally wrong way to do this? I have read some posts about it, but could not manage to go through this: here, and here, and here and here and more.

This example full project can be downloaded Here

Upvotes: 1

Views: 725

Answers (2)

Yeshurun Kubi
Yeshurun Kubi

Reputation: 136

What solve this issue for now, is following @Andy suggestion to use MVVM approach. It was a bit complicated for me, and had to do some learning around.

What I did:

  1. Cancaled the UserControl.
  2. Created a class GlyphModel. That represents each glyph and it's information.
  3. Created a class GlyphViewModel. That builds an ObservableCollection list.
  4. Set the design for the GlyphModel as a ListBox.ItemTemplate.

So now GlyphModel class, implants INotifyPropertyChanged and looks like this:

public GlyphModel(int index)
        {
            _OriginalIndex = index;
        }

        #region Private Members

        private int _OriginalIndex;

        #endregion Private Members

        public int OriginalIndex
        {
            get { return _OriginalIndex; }
            set
            {
                _OriginalIndex = value;
                OnPropertyChanged("OriginalIndex");
            }
        }

        public string Hex => "U+" + OriginalIndex.ToString("X4");

        public string Index => OriginalIndex.ToString();

        public string Glyph => ((char)OriginalIndex).ToString();

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion INotifyPropertyChanged Members

And GlyphViewModel class looks like this:

public static ObservableCollection<GlyphModel> GlyphModelCollection { get; set; } = new ObservableCollection<GlyphModel>();

public static ObservableCollection<string> StringCollection { get; set; } = new ObservableCollection<string>();

        public GlyphViewModel(int rounds)
        {
            for (int i = 33; i < rounds; i++)
            {
                GlyphModel glyphModel = new GlyphModel(i);
                GlyphModelCollection.Add(glyphModel);
                StringCollection.Add($"Index {glyphModel.Index}, Hex {glyphModel.Hex}:  {glyphModel.Glyph}");
            }
        }

In the MainWindow XML I have defined the list with DataTemplate:

<ListBox.ItemTemplate>
          <DataTemplate>
            <Border Style="{StaticResource BorderMouseOver}">
              <Grid>
                <Grid.RowDefinitions>
                  <RowDefinition Height="2*"></RowDefinition>
                  <RowDefinition></RowDefinition>
                  <RowDefinition></RowDefinition>
                </Grid.RowDefinitions>
                <TextBlock HorizontalAlignment="Center" Foreground="White" FontSize="40" Text="{Binding Glyph}" />
                <TextBlock HorizontalAlignment="Center" Foreground="OrangeRed" Text="{Binding Index}" Grid.Row="1" />
                <TextBlock HorizontalAlignment="Center" Foreground="White" Text="{Binding Hex}" Grid.Row="2" />
              </Grid>
            </Border>
          </DataTemplate>
        </ListBox.ItemTemplate>

And for last set the DataContext for the MainWindow:

 DataContext = new GlyphViewModel(2000);

It does work, and works very fast even for 4000 glyphs. Hope this is the right way for doing that. enter image description here

Upvotes: 0

MarioWu
MarioWu

Reputation: 73

For your case, you can create DependencyProperty in your user control like so (just an example).

    #region DP

    public int OrgIndex
    {
        get => (int)GetValue(OrgIndexProperty);
        set => SetValue(OrgIndexProperty, value);
    }

    public static readonly DependencyProperty OrgIndexProperty = DependencyProperty.Register(
        nameof(OrgIndex), typeof(int), typeof(MyUserControl));

    #endregion

And other properties can be set as DP or handle in init or loaded event... Then use your usercontrol in listbox as itemtemplate...

    <ListBox
        Grid.Row="1"
        Margin="10"
        ItemsSource="{Binding IntCollection}"
        VirtualizingPanel.IsVirtualizing="True"
        VirtualizingPanel.VirtualizationMode="Recycling">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <local:MyUserControl OrgIndex="{Binding Path=.}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

And in your vm, create simple type list

    public static ObservableCollection<int> IntCollection { get; set; } = new ObservableCollection<int>();

        for (int i = 0; i < rounds; i++)
        {
            IntCollection.Add(i + 33);
        }

It's quite faster than create a usercontrol list, and you can have your usercontrol and its style as a listviewitem

Upvotes: 1

Related Questions