1

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

Yeshurun Kubi
  • 107
  • 1
  • 10
  • 1
    If the user has to scroll to see any of these then you should use an mvvm approach and data templating to instantiate these usercontrols. You can then virtualise and only construct those visible. – Andy Feb 27 '22 at 11:02
  • @Andy - thanks for your reply. On the real App these usercontrols suppose to be in a `WrapPanel`, but it is true that the user will need scroll. As I mentioned: **I am not fully familiar with WPF work around**, and it will be great help if you can guide a little further about this example how to take in **"mvvm approach and data templating"**. Could not find a post with helpful answer. – Yeshurun Kubi Feb 27 '22 at 11:35
  • 1
    You can just use an itemspaneltemplate to generate your usercontrol per item. Bind an observablecollection or list of item viewmodel. The item viewmodel provides all the info your usercontrol needs via binding. – Andy Feb 27 '22 at 14:05

2 Answers2

1

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

MarioWu
  • 73
  • 1
  • 13
  • 1
    Great thanks for your help! It looks great, and simple. I have manage to implant that, and I the `ListBox` is shown in the `MainWindow` with items of the `UserControl`. But each item of the usercontrol does not update for each int value int the `IntCollection`. Looks like somthing is the binding of `OrgIndex` does not work properly: ``. – Yeshurun Kubi Mar 03 '22 at 07:52
  • @YeshurunKubi yes, that's how I Style my listview item if it has some complex style or structure, etc... and yes, that databinding part could be somewhat wrong. I didn't test it yet... – MarioWu Mar 03 '22 at 09:36
  • for making it an expectable answer, I wish you could test it, for a *correct binding* method. Otherwise I will have to adapt _Andy's_ way of MVVM - that works, but is much more complicated than your solution. – Yeshurun Kubi Mar 03 '22 at 16:56
0

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

Yeshurun Kubi
  • 107
  • 1
  • 10