1

I generate UI elements at runtime by using a ItemsControl. The UI generates successfully but if I am unable to get any properties of the generated UI items, such as "Content" for a label, or SelectedItem for a ComboBox. I tried to get these properties by using this tutorial , and these answers but I always get a NullReferenceException.

The ItemsControl in XAML looks like this:

            <ItemsControl Name="ListOfVideos">
                <ItemsControl.Background>
                    <SolidColorBrush Color="Black" Opacity="0"/>
                </ItemsControl.Background>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="0,0,0,10">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="180"/>
                                <ColumnDefinition Width="400"/>
                                <ColumnDefinition Width="200"/>
                            </Grid.ColumnDefinitions>
                            <Image HorizontalAlignment="Left" Height="100" Width="175" x:Name="VideoThumbnailImage" Stretch="Fill" Source="{Binding VideoThumbnailURL}" Grid.Column="0"></Image>
                            <Label x:Name="VideoTitleLabel" Content="{Binding VideoTitleText}" Foreground="White" Grid.Column="1" VerticalAlignment="Top" FontSize="16" FontWeight="Bold"></Label>
                            <Label x:Name="VideoFileSizeLabel" Content="{Binding VideoTotalSizeText}" Foreground="White" FontSize="14" Grid.Column="1" Margin="0,0,0,35" VerticalAlignment="Bottom"></Label>
                            <Label x:Name="VideoProgressLabel" Content="{Binding VideoStatusText}" Foreground="White" FontSize="14" Grid.Column="1" VerticalAlignment="Bottom"></Label>
                            <ComboBox x:Name="VideoComboBox" SelectionChanged="VideoComboBox_SelectionChanged" Grid.Column="2" Width="147.731" Height="20" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,50" ItemsSource="{Binding VideoQualitiesList}"></ComboBox>
                            <Label Content="Video Quality" Foreground="White" FontSize="14" VerticalAlignment="Top" Grid.Column="2" HorizontalAlignment="Center"></Label>
                            <Label Content="Audio Quality" Foreground="White" FontSize="14" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,27" Grid.Column="2"></Label>
                            <Slider x:Name="VideoAudioSlider" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Bottom" Width="147.731" Maximum="{Binding AudioCount}"></Slider>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

This is how I generate the UI elements

public class VideoMetadataDisplay
    {
        public string VideoTitleText { get; set; }
        public int AudioCount { get; set; }
        public string VideoThumbnailURL { get; set; }
        public string VideoStatusText { get; set; }
        public string VideoTotalSizeText { get; set; }
        public List<string> VideoQualitiesList { get; set; }
    }

public partial class PlaylistPage : Page
{
private void GetPlaylistMetadata()
        {

            List<VideoMetadataDisplay> newList = new List<VideoMetadataDisplay>();
            //populate the list
            ListOfVideos.ItemsSource = newList;
        }
}

And this is how I'm trying to get the properties of the UI elements

public class Utils
    {
        public childItem FindVisualChild<childItem>(DependencyObject obj)
     where childItem : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is childItem)
                {
                    return (childItem)child;
                }
                else
                {
                    childItem childOfChild = FindVisualChild<childItem>(child);
                    if (childOfChild != null)
                        return childOfChild;
                }
            }
            return null;
        }
    }

private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UIElement CurrentItem = (UIElement)ListOfVideos.ItemContainerGenerator.ContainerFromItem(ListOfVideos.Items.CurrentItem);
            Utils utils = new Utils();
            ContentPresenter CurrentContentPresenter = utils.FindVisualChild<ContentPresenter>(CurrentItem);
            DataTemplate CurrentDataTemplate = CurrentContentPresenter.ContentTemplate;
            Label VideoTitle = (Label)CurrentDataTemplate.FindName("VideoTitleLabel", CurrentContentPresenter);
            string VideoTitleText = VideoTitle.Content.ToString();
            MessageBox.Show(VideoTitleText);
        }

Every time I try to run this, FindVisualChild always returns one of the labels (VideoTitleLabel) instead of returning the ContentPresenter for the currently active item. CurrentDataTemplate is then null and I am unable to get any of the UI elements from it.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • Did you know that you can get the text value of `VideoTitleLabel` directly from the current selected item? You don't need to search the Label for this. Always follow the data and not the controls. WPF is designed to build UI around data models. That's why it is strongly recommended to implement the MVVM pattern. Also, It is impossible that `FindVisualChild` returns a `Label`. `FindVisualChild` casts the result to `ContentPresenter`. Since `Label` is not a `ContentPresenter` this would throw an exception. – BionicCode May 31 '20 at 18:26

1 Answers1

1

It is impossible that FindVisualChild<ContentPresenter> returns a Label instance. FindVisualChild casts the result to ContentPresenter. Since Label is not a ContentPresenter, this would throw an InvalidCastException. But before this, child is childItem would return false in case child is of type Label and the generic parameter type childItem is of type ContentPresenter and therefore a potential null is returned.

Short Version

Accessing the the DataTemplate or looking up controls just to get their bound data is always too complicated. It's always easier to access the data source directly.
ItemsControl.SelectedItem will return the data model for the selected item. You are usually not interested in the containers.

private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var listView = sender as ListView;
  var item = listView.SelectedItem as VideoMetadataDisplay;
  MessageBox.Show(item.VideoTitleText);
}

Your Version (FindVisualChild improved)

FindVisualChild is weakly implemented. It will fail and throw an exception if traversal encounters a child node without children i.e. the parameter obj is null. You have to check the parameter obj for null before invoking VisualTreeHelper.GetChildrenCount(obj), to avoid a null reference.

Also you don't need to search the element by accessing the template. You can look it up directly in the visual tree.
I have modified your FindVisualChild method to search elements by name. I also have turned it into an extension method for convenience:

Extension Method

public static class Utils
{
  public static bool TryFindVisualChildByName<TChild>(
    this DependencyObject parent,
    string childElementName,
    out TChild childElement,
    bool isCaseSensitive = false)
    where TChild : FrameworkElement
  {       
    childElement = null;

    // Popup.Child content is not part of the visual tree.
    // To prevent traversal from breaking when parent is a Popup,
    // we need to explicitly extract the content.
    if (parent is Popup popup)
    {
      parent = popup.Child;
    }

    if (parent == null)
    {
      return false;
    }

    var stringComparison = isCaseSensitive 
      ? StringComparison.Ordinal
      : StringComparison.OrdinalIgnoreCase;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
    {
      DependencyObject child = VisualTreeHelper.GetChild(parent, i);
      if (child is TChild resultElement 
        && resultElement.Name.Equals(childElementName, stringComparison))
      {
        childElement = resultElement;
        return true;
      }

      if (child.TryFindVisualChildByName(childElementName, out childElement))
      {
        return true;
      }
    }

    return false;
  }
}

Example

private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var listView = sender as ListView;
  object item = listView.SelectedItem;
  var itemContainer = listView.ItemContainerGenerator.ContainerFromItem(item) as ListViewItem;

  if (itemContainer.TryFindVisualChildByName("VideoTitleLabel", out Label label))
  {
    var videoTitleText = label.Content as string;
    MessageBox.Show(videoTitleText);
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thanks for the answer! Your code worked, but I just want to say 2 things. – Fahad Suhail Ahmad Jun 01 '20 at 14:12
  • I am glad I could help you. – BionicCode Jun 01 '20 at 14:16
  • Thanks for the answer! Your code worked, but I just want to say 2 things. 1) Im not 100% sure, but i think in the extension method it should be `if (child.TryFindVisualChildByName(childElementName, out childElement))` instead of `if (child.TryFindVisualChild(childElementName, out childElement))` , and also while calling the method it should be `if (itemContainer.TryFindVisualChildByName("VideoTitleLabel", out Label label))` instead of `if (itemContainer.TryFindVisualChild("VideoTitleLabel", out Label label))`. – Fahad Suhail Ahmad Jun 01 '20 at 14:19
  • Yes, you are absolutely right. I fixed the typos. They were copy&paste errors. Thank you for pointing this out! – BionicCode Jun 01 '20 at 14:25