0

I want to get all the UI-items from an ItemsControl.

From this post How do I access the children of an ItemsControl? I copied an answer and it works so far.

However, if I DIRECTLY execute the code in the for-loop after setting the ItemsSource (like in the bottom example), the contentpresenter is null and I cannot work with it.
If I run the for-loop quite a while later (maybe when I hit a button), everything works out fine.

How can I access all Children of a ItemsControl, DIRECTLY after setting the ItemsSource?

itemscontrol.ItemsSource = items; // items is a list
itemscontrol.ApplyTemplate(); // might refresh the itemscontrol

for (int i = 0; i < itemscontrol.Items.Count; i++)
{
    //                      ↓ this is null
    ContentPresenter contentpresenter = (ContentPresenter)itemscontrol.ItemContainerGenerator.ContainerFromItem(itemscontrol.Items[i]);
    //                      ↑ this is null

    contentpresenter.ApplyTemplate();

    StackPanel stackPanel = (StackPanel)contentpresenter.ContentTemplate.FindName("selectedStackpanel", contentpresenter);
    // do some stuff with the stackpanel
}
Pixel_95
  • 954
  • 2
  • 9
  • 21
  • What are you actually trying to achieve? There shouldn't be any need to access the ContentPresenter or its child elements in code behind. – Clemens Feb 23 '21 at 09:46
  • When you change itemssource, immediately after that the ui will be templating that data into ui. So DIRECTLY isn't an option. I agree with Clemens anyhow. Tell us what end result you're aiming for. There will almost certainly be a better way than what you're trying to do. – Andy Feb 23 '21 at 10:04
  • There is not enough context to help you. Generally, `ItemContainerGenerator` can only return valid item containers, if they are generated. Otherwise the result will be `null`. Item containers are generated/available when the `ItemsControl` completes the layout procedure and is about to get rendered. It seems like you have a timing problem, but who knows. Not enough details. I agree, accessing the item containers is very rarely required. _"do some stuff with the stackpanel"_ in code-behind also sounds like another bad idea. Looks like you are over complicating heavily. Maybe you don't know XAML – BionicCode Feb 23 '21 at 14:18
  • I have a list with simulations models, which are all in the itemscontrol and display each in its own stackpanel. one of them is the "users selected" model (has a certain ID) and should appear in a different color. thats way I want to brush all stackpanels in white except for the selected one (this will appear in red) After setting the models I want to brush all in their color. however, if i do it directly after setting the element, it returns null. I found a solution by using the StatusChanged method form the `ItemContainerGenerator` but if you have a better solution I'm open for them! – Pixel_95 Feb 23 '21 at 17:05
  • The better solution is to add related attributes to your item model e.g. a `IsUserSelected` property. Then create a `Style`, which you assign to `ItemsControl.ItemContainerStyle`. Inside this Style you define a trigger that triggers on `IsUserSelected`. That's how it is done. Don't deal with the generator and check if each item is generated. Let the framework do this work for you. – BionicCode Feb 23 '21 at 17:13

2 Answers2

1

The better solution is to add related attributes to your item model e.g. a IsUserSelected property. Then create a Style, which you assign to ItemsControl.ItemContainerStyle. Inside this Style you define a trigger that triggers on IsUserSelected.

That's how it is done. Don't deal with the generator and check if each item is generated. Let the framework do this work for you.

 <ListBox ItemsSource="{Binding Items}">
   <ListBox.ItemContainerStyle>
     <Style TargetType="ListBoxItem">

       <Style.Triggers>
         <DataTrigger Binding="{Binding IsUserSelected}"
                      Value="True">
           <Setter Property="Background" Value="Red" />
         </DataTrigger>
       </Style.Triggers>
     </Style>
   </ListBox.ItemContainerStyle>`enter code here`
 </ListBox>

Since you already have a property HighlightId in your code-behind file, you can use a IMultiValueConverter together with a MultiBinding to define a color based on the value:

MainWindow.xaml.cs

partial class MainWindow
{
  public static readonly DependencyProperty HighlightIdProperty = DependencyProperty.Register(
    "HighlightId",
    typeof(int),
    typeof(MainWindow),
    new PropertyMetadata(default(int)));

  public int HighlightId
  {
    get => (int) GetValue(MainWindow.HighlightIdProperty);
    set => SetValue(MainWindow.HighlightIdProperty, value);
  }
}

HighlightIdToBrushConverter.cs

public class HighlightIdToBrushConverter : IMultiValueConverter
{
  public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
  {
    if (!(values[0] is MyModelType currentItem 
      && values[1] is int highlightId))
    {
      return Binding.DoNothing;
    }
    
    var highlightBrush = highlightId == currentItem.Id
      ? new SolidColorBrush(Colors.Red)
      : new SolidColorBrush(Colors.Transparent);
      
     highlightBrush.Freeze();
     return highlightBrush; 
  }

  public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
    throw new NotSupportedException();
}

MainWindow.xaml

<ListBox ItemsSource="{Binding Items}">
  <ListBox.Resources>
    <HighlightIdToBrushConverter x:Key="HighlightIdToBrushConverter" />
  </ListBox.Resources>

  <ListBox.ItemContainerStyle>
    <Style TargetType="ListBoxItem">
      <Setter Property="Background">
        <Setter.Value>
          <MultiBinding Converter="{StaticResource HighlightIdToBrushConverter}">
            <Binding />
            <Binding RelativeSource="{RelativeSource AncestorType=Window}" 
                     Path="HighlightId" />
          </MultiBinding>
        </Setter.Value>
      </Setter>
    </Style>
  </ListBox.ItemContainerStyle>
</ListBox>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • thx! in my `.xaml.cs` I have an integer `highlightID` containing which item should be highlighted. can I do something like `{Binding thisItem.ID == highlightID}` directly within the xaml? – Pixel_95 Feb 23 '21 at 19:06
  • thx a lot! thats nice! but its a huge amount of code for such a simple task ... something like simply doing an `if` statement in `xaml` for the background brush is not possible, is it? – Pixel_95 Feb 25 '21 at 19:48
  • I posted you a solution of mine down. I believe you that this is not the most beautiful way, but it actually works quite reliable. – Pixel_95 Feb 25 '21 at 19:57
  • This is quite simple. You said you already have this `highlightID` property. So you only have to move the if-statement to the converter and setup a binding in the Style - that's all. No resource lookup or waiting for containers to be generated and no additional for-loop over all items, no forcing of template generation, no magic strings - framework will do all of this. If reliability and performance is your concern, then don't do it your way. if you can't handle the two lines of XAML, then go your way. Note that your code will only work with the plain `ItemsControl` and not for `ListBox` etc. – BionicCode Feb 25 '21 at 20:33
  • i tried to implement your way and it tells me HighlightIdToBrushConverter is not supported in a WPF project (line 3 in your xaml) – Pixel_95 Feb 28 '21 at 11:29
  • 1
    Why shouldn't it? Have you qualified the type with its namespace? It should be for example `` . The code is tested and it works. So it must be on your end. The example does not use namespace qualifiers to avoid confusion (at least that was the intention, as I assumed that using namespaces for custom types should be apparent). – BionicCode Feb 28 '21 at 11:53
  • now it tells me "ListBoxItem" is not found in itemsControl (since I use itemscontrol instead of listebox) and the property "ItemsControlItem" does not exist (nor present in the suggestions) – Pixel_95 Feb 28 '21 at 12:18
  • Why? Don't you need scrolling? You should consider `ItemsControl` more of a base class to create custom classes. Better use ListBox or ListView. It is more specialized and provides better performance as it uses UI virtualization by default. – BionicCode Feb 28 '21 at 12:33
  • Aside from that, the ItemsControl does not use a special container. It simply uses a ContentPresenter (there is no ItemsControItem, that's true for this reason). – BionicCode Feb 28 '21 at 12:34
0

Maybe not the best coding style, but it works reliably.

public ClassConstructor()
{
    InitializeComponent();
    itemscontrol.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}

...

void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
    if (itemscontrol.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
    {
        for (int i = 0; i < itemscontrol.Items.Count; i++)
        {
            ContentPresenter contentpresenter = (ContentPresenter)itemscontrol.ItemContainerGenerator.ContainerFromItem(itemscontrol.Items[i]);
            contentpresenter.ApplyTemplate();
            StackPanel stackPanel = (StackPanel)contentpresenter.ContentTemplate.FindName("selectedStackpanel", contentpresenter);
            int ID = FindIDofSelectedItemFromStackpanel(stackPanel);

            if (highlightID == ID)
            {
                Border border = (Border)contentpresenter.ContentTemplate.FindName("border_models", contentpresenter);
                border.Background = (Brush)FindResource("brushAccent3");
            }
            else
            {
                Border border = (Border)contentpresenter.ContentTemplate.FindName("border_models", contentpresenter);
                border.Background = Brushes.White;
            }
        }
    }
}
Pixel_95
  • 954
  • 2
  • 9
  • 21