Шаблон проектирования — WPF автоматически добавляет AttachedProperty к родительскому

#c# #wpf #dependency-properties #attached-properties

#c# #wpf #зависимость-свойства #прикрепленные свойства

Вопрос:

Я создал пользовательский UserControl, который является своего рода кнопкой виртуальной клавиатуры. Я ищу следующее функционально:

  • Любая панель должна содержать коллекцию всех дочерних элементов кнопок клавиатуры.
  • Любой другой элемент управления, который также является дочерним элементом этой панели, должен иметь возможность перечислять кнопки клавиатуры своего родителя.

Функционально достичь этого не составляет большой проблемы. Я мог бы создать свойство Attached-DependencyProperty типа List и вручную управлять этим списком. Проблема такого подхода заключалась бы в том, что он довольно подвержен ошибкам и неудобен.

Есть ли какая-либо возможность автоматически присоединять кнопку клавиатуры во время создания к AttachedProperty родительского элемента?

Ответ №1:

Как вы упомянули, ведение списка подвержено ошибкам — вам нужно будет синхронизировать его с визуальным деревом WPF. Вместо того, чтобы поддерживать список, вы можете перемещаться по визуальному дереву из заданного корня. В вашем случае это звучит так, как будто root — это Panel .

Перемещаться по визуальному дереву довольно просто. В этом посте показано несколько примеров. Тот, который, я думаю, имеет отношение, это:

 public static IEnumerable<DependencyObject> GetVisualTree(this DependencyObject element)
{
    int childrenCount = VisualTreeHelper.GetChildrenCount(element);

    for (int i = 0; i < childrenCount; i  )
    {
        var visualChild = VisualTreeHelper.GetChild(element, i);

        yield return visualChild;

        foreach (var visualChildren in GetVisualTree(visualChild))
        {
            yield return visualChildren;
        }
    }
}
  

Это выполняет полный обход всех визуальных дочерних элементов данного DependencyObject . Таким образом, для любого заданного Panel вы можете вызвать этот метод. Поскольку это IEnumerable , вы можете использовать LINQ для фильтрации результатов по вашему пользовательскому UserControl.

Для вашего другого требования («Любой другой элемент управления, который также является дочерним элементом этой панели, должен иметь возможность перечислять кнопки клавиатуры своего родителя»): Учитывая дочерний элемент a Panel , пройдитесь по визуальному дереву вверх, чтобы найти первый Panel , и используйте тот же процесс, чтобы получить все соответствующие дочерние элементы.

Если вам действительно нужно это как DependencyProperty (например, для привязки данных), я думаю, вы могли бы сделать это доступным только для чтения и по-прежнему использовать метод визуального обхода дерева в getter, но я не уверен.

Ответ №2:

Я уже создал довольно хорошее решение для своей проблемы. Следующий код показывает мою реализацию:

 // KeyboardButton.cs
/// <remarks>
/// This class does contain a lot of more code which isn't related to the actual problem.
/// </remarks>
public partial class KeyboardButton
{
    [DefaultValue(Key.NoName)]
    public Key EmulatedKey { get; set; }
}


// KeyboardPanel.cs
public class KeyboardButtonCollection : Collection<KeyboardButton> { }


public static class KeyboardPanel
{
    internal const string ButtonsPropertyName = "Buttons";


    static KeyboardPanel()
    {
        ButtonsProperty = DependencyProperty.RegisterAttached
        (
            ButtonsPropertyName,
            typeof(KeyboardButtonCollection),
            typeof(KeyboardPanel),
            new UIPropertyMetadata(null, OnButtonsChanged)
        );
    }


    public static KeyboardButtonCollection GetButtons(DependencyObject dependencyObject)
    {
        return (KeyboardButtonCollection)dependencyObject.GetValue(ButtonsProperty);
    }

    public static void SetButtons(DependencyObject dependencyObject, KeyboardButtonCollection value)
    {
        dependencyObject.SetValue(ButtonsProperty, value);
    }


    public static bool IsKeyboardPanel(DependencyObject dependencyObject)
    {
        return GetButtons(dependencyObject).Count != 0;
    }


    public static readonly DependencyProperty ButtonsProperty;


    private static void OnButtonsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        bool isSupportedType = true;

        if (dependencyObject is Panel)
        {
            var panel = (Panel)dependencyObject;
            foreach (var button in (KeyboardButtonCollection)e.NewValue)
                panel.Children.Add(button);
        }
        else
            isSupportedType = false;

        if (!isSupportedType)
            throw new NotSupportedException(string.Format("Type {0} is not supported as KeyboardPanel.", dependencyObject.GetType()));
    }
}


<!-- MainWindow.xaml -->
<Grid>
    <controls:KeyboardPanel.Buttons>
        <controls:KeyboardButtonCollection>
            <controls:KeyboardButton Content="Enter" EmulatedKey="Enter"/>
            <controls:KeyboardButton Grid.Row="1" Content="Some Key"/>
            <controls:KeyboardButton Grid.Row="2" Content="Another Key"/>
        </controls:KeyboardButtonCollection>
    </controls:KeyboardPanel.Buttons>

    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <Button Grid.Row="3" Content="Test" Loaded="Button_Loaded"/>
</Grid>


// MainWindow.xaml.cs
public partial class MainWindow : Window
{
    private void Button_Loaded(object sender, RoutedEventArgs e)
    {
        var button = (Button)sender;
        if (KeyboardPanel.IsKeyboardPanel(button.Parent))
        {
            var enterButton = KeyboardPanel.GetButtons(button.Parent)
                                           .FirstOrDefault(b => b.EmulatedKey == Key.Enter);

            if (enterButton != null)
                MessageBox.Show("KeyboardPanel contains an EnterButton");
            else
                MessageBox.Show("KeyboardPanel doesn't contain an EnterButton");
        }
    }
}
  

Обычно я не программирую на .NET, но — время от времени — я «вынужден» создавать некоторые инструменты с его помощью. Однако, некоторые из вас, ребята, могут знать, как я могу устранить недостаток этого кода: он не поддерживает VisualStudio или Expression Blend Designer. Таким образом, элементы управления не могут быть изменены или даже просмотрены во время разработки.

Комментарии:

1. Я только что узнал, что дизайнер показывает прикрепленные элементы управления, когда на эту панель не добавлен другой элемент. Похоже, это ошибка дизайнера.