다음을 통해 공유


WPF: get all controls of a specific type using C#

Introduction

In this article language extension methods are presented to locate controls in containers such as a windows, grid or StackPanel for performing business logic like obtaining a selection from a group of Checkboxes, a selected RadioButton in a group or enabling/disabling specific types of controls.

Base extension method

The base language extension method given a container for dependencyItem parameter will recursively iterate all controls in the container returning an IEnumerable of T.

public static  IEnumerable<T> Descendants<T>(DependencyObject dependencyItem) where T : DependencyObject
{
    if (dependencyItem != null)
    {
        for (var index = 0; index < VisualTreeHelper.GetChildrenCount(dependencyItem); index++)
        {
            var child = VisualTreeHelper.GetChild(dependencyItem, index);
            if (child is T dependencyObject)
            {
                yield return  dependencyObject;
            }
 
            foreach (var childOfChild in Descendants<T>(child))
            {
                yield return  childOfChild;
            }
        }
    }
}

It's purpose is to iterate all controls and meant to be called with conditions e.g. get all TextBox controls. By itself would not really serve any real purpose. Example, asking for all control in a grid for the following window would return 210 controls.

Also, rather than call the base extension a wrapper would be better with a name that indicates the purpose.

public static  List<Control> AllControls(this DependencyObject control) =>
    Descendants<Control>(control).ToList();

Beginning call from the window's grid in a button click.

List<Control> allControls = MainGrid.AllControls();

Wrapper extension methods

CheckBoxes

 In a real world example a series of CheckBoxes may be presented to get user selections.  In the following example this is the XAML to present several CheckBox controls, a button to get selections using CheckBox controls. Focus is on the grid CheckBoxGrid.

<Window x:Class="WpfAppExample1.CheckBoxWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppExample1"
        mc:Ignorable="d"
        Title="CheckBoxes"
        Height="227.571"
        Width="206"
        WindowStartupLocation="CenterScreen"
        WindowStyle="ToolWindow">
 
    <Grid Name="CheckBoxGrid">
        <CheckBox Content="C#"
                  Height="16"
                  HorizontalAlignment="Left"
                  Margin="20,20,0,0"
                  Name="sharpCheckBox"
                  VerticalAlignment="Top" />
 
        <CheckBox Content="VB.NET"
                  Height="16"
                  HorizontalAlignment="Left"
                  Margin="20,51,0,0"
                  Name="VisualBasicCheckBox"
                  VerticalAlignment="Top" />
 
        <CheckBox Content="F#"
                  Height="16"
                  HorizontalAlignment="Left"
                  Margin="20,85,0,0"
                  Name="functionCheckBox"
                  VerticalAlignment="Top" />
 
        <CheckBox Content="None"
                  Height="16"
                  HorizontalAlignment="Left"
                  Margin="20,119,0,0"
                  Name="NoneCheckBox"
                  VerticalAlignment="Top" />
 
        <Button x:Name="GetCheckedButton"
                Content="Get checked"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Width="150"
                Margin="20,156,0,0"
                Click="GetCheckedButton_Click"/>
 
    </Grid>
</Window>

In the button click event the first extension determines if there are any CheckBox control checked which makes a call to another extension method to get a list of CheckBox which obtains information from the base extension method.

/// <summary>
/// Determine if any CheckBoxes are checked in container
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static  bool CheckListAnyChecked(this DependencyObject control) => 
    control.CheckBoxListChecked().Any();

Get all CheckBoxes from the base extension method with a Where condition.

/// <summary>
/// Get all ComboBox controls in a container
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static  List<ComboBox> ComboBoxList(this DependencyObject control) => 
    Descendants<ComboBox>(control).ToList();

This checks to see if there are checked CheckBox controls and presents their text.

private void  GetCheckedButton_Click(object sender, RoutedEventArgs e)
{
    if (CheckBoxGrid.CheckListAnyChecked())
    {
        var result = string.Join("\n",
            CheckBoxGrid.CheckBoxListChecked()
                .Select(cb => cb.Content.ToString())
                .ToArray());
 
        MessageBox.Show($"Selected languages\n{result}");
 
    }
    else
    {
        MessageBox.Show("Nothing has been selected.");
    }
}

An alternate approach which first gets a list of CheckBox controls, applies a Where condition then if there are checked items present them. The main difference is the list of CheckBox controls is retrieved once rather than twice as per the first example.

private void  GetCheckedButton_Click(object sender, RoutedEventArgs e)
{
    var checkBoxes = CheckBoxGrid.CheckBoxListChecked();
    if (checkBoxes.Any(cb => cb.IsChecked == true))
    {
        var result = string.Join("\n",
            checkBoxes
                .Select(cb => cb.Content.ToString())
                .ToArray());
 
        MessageBox.Show($"Selected languages\n{result}");
 
    }
    else
    {
        MessageBox.Show("Nothing has been selected.");
    }
}

Radio Button groups

A common operation is to present a group of radio buttons in a group, sometimes multiple groups of radio buttons. Using the same approach as with CheckBox extension method wrapper over the base extension the same may be done with radio buttons.

In this example there are two groups of radio buttons.

<Window x:Class="WpfAppExample1.RadioButtonWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppExample1"
        mc:Ignorable="d"
        Title="Radio Buttons"
        Height="225"
        Width="314"
        WindowStartupLocation="CenterScreen"
        WindowStyle="ToolWindow">
    <Grid>
         
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="13*"/>
            <ColumnDefinition Width="5*"/>
        </Grid.ColumnDefinitions>
 
        <Border Padding="10" Grid.ColumnSpan="2">
            <StackPanel x:Name="Stacker1">
 
                <RadioButton x:Name="Windows8RadioButton"
                             GroupName="OperatingSystemGroup"
                             Content="Windows 8"
                             IsChecked="True"/>
 
                <RadioButton x:Name="Windows7RadioButton"
                             GroupName="OperatingSystemGroup"
                             Content="Windows 7" />
 
                <RadioButton x:Name="Windows10RadioButton"
                             GroupName="OperatingSystemGroup"
                             Content="Windows 10" />
 
                <RadioButton x:Name="Office2007RadioButton"
                             GroupName="OfficeGroup"
                             Content="Microsoft Office 2007"
                             IsChecked="True"/>
 
                <RadioButton x:Name="Office2010RadioButton"
                             GroupName="OfficeGroup"
                             Content="Microsoft Office 2010"/>
 
                <RadioButton x:Name="OpenOfficeRadioButton"
                             GroupName="OfficeGroup"
                             Content="Open Office"/>
 
            </StackPanel>
        </Border>
 
        <Button Grid.Column="0"
                x:Name="GetCheckedRadioButtons"
                Content="Radio button list"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Width="137"
                Margin="10,120,0,0"
                Click="GetCheckedRadioButtons_Click"/>
 
        <Button Grid.Column="0"
                x:Name="GetCheckedRadioButtonsByGroupName"
                Content="Selected by group"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Width="137" Margin="10,147,0,0"
                Click="GetCheckedRadioButtonsByGroupName_Click"/>
 
    </Grid>
</Window>



To get the selected radio button for office version which is in a group the following extension method is used to work off a specific group.

public static  RadioButton SelectedRadioButtonInGroup(this DependencyObject control, string groupName) => 
    Descendants<RadioButton>(control)
        .FirstOrDefault(radioButton => 
            radioButton.IsChecked == true  && radioButton.GroupName == groupName);

In this case the container is a StackPanel named Stacker1.

private void  GetCheckedRadioButtonsByGroupName_Click(object sender, RoutedEventArgs e)
{
    var selectedOperatingSystem = Stacker1
        .SelectedRadioButtonInGroup("OperatingSystemGroup");
 
    MessageBox.Show($"Operating system is {selectedOperatingSystem.Content}");
}

To get all radio buttons regardless of a group.

public static  List<RadioButton> RadioListAreChecked(this DependencyObject control) => 
    Descendants<RadioButton>(control)
        .Where(radioButton => radioButton.IsChecked == true).ToList();

Other controls

What applies to the presented extensions targeting specific controls in containers applies to TextBox, TextBlock and other controls.

/// <summary>
/// Get all TextBox controls in a container
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static  List<TextBox> TextBoxList(this DependencyObject control) => 
    Descendants<TextBox>(control).ToList();
 
/// <summary>
/// Get all TextBoxBlock controls in a container
/// </summary>
/// <param name="control"></param>
/// <returns></returns>
public static  List<TextBlock> TextBoxBlockList(this DependencyObject control) => 
    Descendants<TextBlock>(control).ToList();

Enabling/disabling controls

Business logic may dictate that specific controls should be enabled or disabled which can be done using extensions also, for instance the following can enable or disable all CheckBox controls in a container called as needed,

/// <summary>
/// Enable or disable all check boxes in a container
/// </summary>
/// <param name="control"></param>
/// <param name="enable"></param>
public static  void EnableCheckBoxes(this DependencyObject control, bool enable = false)
{
    foreach (var checkBox in Descendants<CheckBox>(control))
    {
        checkBox.IsEnabled = enable;
    }
}

In some cases one CheckBox should not be touched, in this case provide a parameter to exclude it.

/// <summary>
/// Enable or disable all CheckBox controls excluding one
/// specified in excludeName parameter.
/// </summary>
/// <param name="control">Parent control</param>
/// <param name="enable">true, false</param>
/// <param name="excludeName">Exclude this control</param>
/// <remarks>
/// An adaptation could be having the last parameter an
/// array of CheckBox names.
/// </remarks>
public static  void EnableCheckBoxesSpecial(this DependencyObject control, bool enable, string excludeName)
{
    foreach (var checkBox in Descendants<CheckBox>(control))
    {
        if (checkBox.Name != excludeName)
        {
            checkBox.IsEnabled = enable;
        }
    }
}

The above may be changed to pass an array of CheckBox names to exclude or even look at the Tag property for a specific value to include or exclude. The following does this with TextBox controls where the Tag = "R". As stated in the summary "R" is a poor choice for a indicator and highly recommend changing it to a meaningful indicator.

/// <summary>
/// Enable or disable a TextBox in a container 
/// 
/// If Tag = R (need a better indicator) this TextBox  IsReadOnly is excluded as it's
/// a read only TextBox always per business rules.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control"></param>
/// <param name="enable"></param>
public static  void EnableTextBoxes<T>(this DependencyObject control, bool enable = false)
{
    foreach (var textBox in Descendants<TextBox>(control))
    {
        if (textBox.Tag?.ToString() == "R")
        {
            continue;
        }
 
        textBox.IsReadOnly = enable;
 
        textBox.Background = textBox.IsReadOnly ?  
            new SolidColorBrush(Color.FromArgb(255, 240, 240, 240)):  
            Brushes.White;
    }
}

The following also includes those controls with a specific tag. In the supplied source code the MainWindow has TextBox controls which grow/shrink by default, this extension method prevents this behavior.

/// <summary>
/// By setting Width this prevents horizontal resizing
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control"></param>
/// <param name="width"></param>
/// <remarks>
/// Tag = A where A means nothing, need to have a better indicator
/// </remarks>
public static  void SetTextBoxUniversalWidth<T>(this DependencyObject control, int width = 32)
{
    foreach (var textBox in Descendants<TextBox>(control))
    {
        if (textBox.Tag?.ToString() == "A")
        {
            textBox.Width = width;
        }
    }
}

Called againsts a StackPanel.

GridStackPanel.SetTextBoxUniversalWidth<StackPanel>();

Windows Forms 

For completeness the following are a base for working with controls in Windows Forms projects. These extensions are used exactly as with the WPF extensions.

using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
 
namespace ControlExtensions
{
    public static  class ControlExtensions
    {
        /// <summary>
        /// Get a collection of a specific type of control from a control or form.
        /// </summary>
        /// <typeparam name="T">Type of control</typeparam>
        /// <param name="control">Control to traverse</param>
        /// <returns>IEnumerable of T</returns>
        public static  IEnumerable<T> Descendants<T>(this Control control) where T : class
        {
            foreach (Control child in control.Controls)
            {
                T thisControl = child as  T;
                if (thisControl != null)
                {
                    yield return  (T)thisControl;
                }
 
                if (child.HasChildren)
                {
                    foreach (T descendant in Descendants<T>(child))
                    {
                        yield return  descendant;
                    }
                }
            }
        }
        public static  List<TextBox> TextBoxList(this Control container) => 
            container.Descendants<TextBox>().ToList();
 
        public static  List<Label> LabelList(this Control container) => 
            container.Descendants<Label>().ToList();
 
        public static  List<DataGridView> DataGridViewList(this Control container) => 
            container.Descendants<DataGridView>().ToList();
 
        public static  List<ListView> ListViewViewList(this Control container) => 
            container.Descendants<ListView>().ToList();
 
        public static  List<CheckBox> CheckBoxList(this Control container) => 
            container.Descendants<CheckBox>().ToList();
 
        public static  List<ComboBox> ComboBoxList(this Control container) => 
            container.Descendants<ComboBox>().ToList();
 
        public static  List<ListBox> ListBoxList(this Control container) => 
            container.Descendants<ListBox>().ToList();
 
        public static  List<DateTimePicker> DateTimePickerList(this Control container) => 
            container.Descendants<DateTimePicker>().ToList();
 
        public static  List<PictureBox> PictureBoxList(this Control container) => 
            container.Descendants<PictureBox>().ToList();
 
        public static  List<Panel> PanelList(this Control container) => 
            container.Descendants<Panel>().ToList();
 
        public static  List<GroupBox> GroupBoxList(this Control container) => 
            container.Descendants<GroupBox>().ToList();
 
        public static  List<Button> ButtonList(this Control container) => 
            container.Descendants<Button>().ToList();
 
        public static  List<RadioButton> RadioButtonList(this Control container) => 
            container.Descendants<RadioButton>().ToList();
 
        public static  List<NumericUpDown> NumericUpDownList(this Control container) => 
            container.Descendants<NumericUpDown>().ToList();
 
        public static  RadioButton RadioButtonChecked(this Control container, bool pChecked = true) =>
            container.Descendants<RadioButton>().ToList()
                .FirstOrDefault((radioButton) => radioButton.Checked == pChecked);
 
        public static  string[] ControlNames(this IEnumerable<Control> container) => 
            container.Select((control) => 
            control.Name).ToArray();
    }
}

Summary

This article has provided language extension methods to access controls in containers such as a Window, a Grid or StackPanel to perform actions against specific controls or obtain information on user selections.

See also

Source code

https://github.com/karenpayneoregon/FindChildrenWpf