SideMenu. Как сделать подпункты

263
05 декабря 2017, 18:25

Привет всем. Ребят, не могу понять, как вообще можно сделать кастомное меню? Вот к примеру у меня есть UserControl:

<!-- Menu Items -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
    <ItemsControl ItemsSource="{Binding ElementName=UC, Path=MenuItems}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <local:SideMenuItem Style="{Binding ItemContainerStyle, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

имеющий в codebehind два свойства зависимости MenuItems( родительская коллекция списка меню ) и ItemContainerStyle( стиль для каждого пункта меню ). Использую это меню вот так:

<local:SideMenu Grid.Column="0" MenuItems="{Binding MenuVM.Categories}">
    <local:SideMenu.ItemContainerStyle>
        <Style TargetType="{x:Type local:SideMenuItem}">
            <Setter Property="NameItem" Value="{Binding Name}"/>
            <Setter Property="Children" Value="{Binding Children}"/>
        </Style>
    </local:SideMenu.ItemContainerStyle>
</local:SideMenu>

У каждого пункта, представляющим класс SideMenuItem, есть тоже два свойства зависимости - Name( заголовок ) и Children( коллекция дочерних элементов меню ).

Все работает нормально. Вот скриншот:

Теперь вопрос в том, как сделать подпункты, чтобы они появлялись по нажатию на родительский пункт относительно него с правой стороны. Типа такого:

Как вообще это делается? Подскажите пожалуйста

Answer 1

Вам не нужно изобретать велосипед, встроенное меню уже есть.

Например, можно сделать так:

<Menu VerticalAlignment="Top" HorizontalAlignment="Left"
      Background="{x:Static SystemColors.ControlLightBrush}">
    <Menu.ItemsPanel>
        <!-- вертикальное расположение для меню верхнего уровня -->
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Vertical"/>
        </ItemsPanelTemplate>
    </Menu.ItemsPanel>
    <Menu.ItemContainerStyle>
        <!-- центрирование элементов -->
        <Style TargetType="MenuItem">
            <Setter Property="HorizontalAlignment" Value="Center"/>
        </Style>
    </Menu.ItemContainerStyle>
    <MenuItem Header="Шкатулки"/>
    <MenuItem Header="Иконы"/>
    <MenuItem Header="Панно">
        <MenuItem Header="Fürstenzug"/>
        <MenuItem Header="Полтавская баталия"/>
    </MenuItem>
    <MenuItem Header="Распятия"/>
</Menu>

Единственное, что тут работает не так — подменю открывается вниз, а не вправо.

К сожалению, прямого управления расположением подменю нету. Но это не большая проблема.

Подменю открывается в отдельном Popup'е. Заглядывая в определение класса MenuItem, мы видим

[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]

что означает, что доступ к этому внутреннему элементу можно получить по имени "PART_Popup".

Для этого проще всего навесить attached property. Идею и реализацию я стащил из этого ответа.

Кладём attached property:

static class MenuExtensions
{
    // стандартное attached property
    public static PlacementMode GetMenuPlacement(MenuItem menu) =>
        (PlacementMode)menu.GetValue(MenuPlacementProperty);
    public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
        menu.SetValue(MenuPlacementProperty, value);
    public static readonly DependencyProperty MenuPlacementProperty =
        DependencyProperty.RegisterAttached(
            "MenuPlacement",
            typeof(PlacementMode),
            typeof(MenuExtensions),
            new FrameworkPropertyMetadata(PlacementMode.Bottom, OnMenuPlacementChanged));
    // вызывается при изменении значения
    static void OnMenuPlacementChanged(
        DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var menuItem = o as MenuItem;
        if (menuItem == null)
            return;
        if (menuItem.IsLoaded) // загружен => шаблон уже применён
        {
            UpdatePopupPlacement(menuItem);
        }
        else                   // иначе дожидаемся конца загрузки
        {
            RoutedEventHandler handler = null;
            handler = (oo, ee) =>
                {
                    UpdatePopupPlacement(menuItem);
                    menuItem.Loaded -= handler;
                };
            menuItem.Loaded += handler;
        }
    }
    static void UpdatePopupPlacement(MenuItem menuItem)
    {
        if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
            popup.Placement = GetMenuPlacement(menuItem);
    }
}

Имея это в своём распоряжении, можно навесить его на MenuItem:

<MenuItem Header="Панно" local:MenuExtensions.MenuPlacement="Right">

(Ну и чтобы не повторять код, можно поместить это в стиль.)

Результат:

Если нам нужно ещё, к примеру, сдвинуть Popup, нужно немного дополнить код. Я прорефакторил MenuExtensions, получилось вот что:

static class MenuExtensions
{
    #region attached property PlacementMode MenuPlacement
    public static PlacementMode GetMenuPlacement(MenuItem menu) =>
        (PlacementMode)menu.GetValue(MenuPlacementProperty);
    public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
        menu.SetValue(MenuPlacementProperty, value);
    public static readonly DependencyProperty MenuPlacementProperty =
        DependencyProperty.RegisterAttached(
            "MenuPlacement",
            typeof(PlacementMode),
            typeof(MenuExtensions),
            new FrameworkPropertyMetadata(
                PlacementMode.Bottom,
                (o, args) => NowOrOnLoaded(o, UpdatePopupPlacement)));
    #endregion
    #region attached property Point MenuOffset
    public static Point GetMenuOffset(MenuItem menu) =>
        (Point)menu.GetValue(MenuOffsetProperty);
    public static void SetMenuOffset(MenuItem menu, Point value) =>
        menu.SetValue(MenuOffsetProperty, value);
    public static readonly DependencyProperty MenuOffsetProperty =
        DependencyProperty.RegisterAttached(
            "MenuOffset",
            typeof(Point),
            typeof(MenuExtensions),
            new PropertyMetadata(
                default(Point),
                (o, args) => NowOrOnLoaded(o, UpdatePopupOffset)));
    #endregion
    static void NowOrOnLoaded(DependencyObject o, Action<MenuItem> a)
    {
        var menuItem = o as MenuItem;
        if (menuItem == null)
            return;
        if (menuItem.IsLoaded)
        {
            a(menuItem);
        }
        else
        {
            RoutedEventHandler handler = null;
            handler = (oo, ee) =>
            {
                a(menuItem);
                menuItem.Loaded -= handler;
            };
            menuItem.Loaded += handler;
        }
    }
    static void UpdatePopupPlacement(MenuItem menuItem)
    {
        if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
            popup.Placement = GetMenuPlacement(menuItem);
    }
    static void UpdatePopupOffset(MenuItem menuItem)
    {
        if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
        {
            var offset = GetMenuOffset(menuItem);
            popup.HorizontalOffset = offset.X;
            popup.VerticalOffset = offset.Y;
        }
    }
}

Им можно пользоваться так:

<MenuItem Header="Панно"
          local:MenuExtensions.MenuPlacement="Right"
          local:MenuExtensions.MenuOffset="15,15"> ...

Обновление: Я обнаружил баг в MenuExtensions (никогда нельзя доверять чужому коду!), и исправил его. Заодно сложный паттерн с подпиской и отпиской заменил на Task.

static class MenuExtensions
{
    #region attached property PlacementMode MenuPlacement
    public static PlacementMode GetMenuPlacement(MenuItem menu) =>
        (PlacementMode)menu.GetValue(MenuPlacementProperty);
    public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
        menu.SetValue(MenuPlacementProperty, value);
    public static readonly DependencyProperty MenuPlacementProperty =
    DependencyProperty.RegisterAttached(
        "MenuPlacement",
        typeof(PlacementMode),
        typeof(MenuExtensions),
        new FrameworkPropertyMetadata(
            PlacementMode.Bottom,
            (o, args) => WhenPopupAvailable(o, UpdatePopupPlacement)));
    #endregion
    #region attached property Point MenuOffset
    public static Point GetMenuOffset(MenuItem menu) =>
        (Point)menu.GetValue(MenuOffsetProperty);
    public static void SetMenuOffset(MenuItem menu, Point value) =>
        menu.SetValue(MenuOffsetProperty, value);
    public static readonly DependencyProperty MenuOffsetProperty =
    DependencyProperty.RegisterAttached(
        "MenuOffset",
        typeof(Point),
        typeof(MenuExtensions),
        new PropertyMetadata(
            default(Point),
            (o, args) => WhenPopupAvailable(o, UpdatePopupOffset)));
    #endregion
    static Task TillEvent(
        Action<RoutedEventHandler> subscribe, Action<RoutedEventHandler> unsubscribe)
    {
        var tcs = new TaskCompletionSource<bool>();
        RoutedEventHandler handler = null;
        handler = (o, e) =>
        {
            tcs.TrySetResult(true);
            unsubscribe(handler);
        };
        subscribe(handler);
        return tcs.Task;
    }
    static async void WhenPopupAvailable(DependencyObject o, Action<Popup, MenuItem> a)
    {
        var menuItem = o as MenuItem;
        if (menuItem == null)
            return;
        if (!menuItem.IsLoaded) // не загружен => дожидаемся
            await TillEvent(h => menuItem.Loaded += h, h => menuItem.Loaded -= h);
        Popup popup = (Popup)menuItem.Template.FindName("PART_Popup", menuItem);
        if (popup == null) // нет ещё попапа?
        {
            var parent = menuItem.Parent as MenuItem;
            if (parent == null) // если нет родителя, тогда непонятна причина, выходим
                return; // для отладки лучше бросить исключение тут
            // если причина в том, что родитель закрыт, подождём пока откроется
            if (!parent.IsSubmenuOpen) 
                await TillEvent(h => parent.SubmenuOpened += h,
                                h => parent.SubmenuOpened -= h);
        }
        popup = (Popup)menuItem.Template.FindName("PART_Popup", menuItem);
        if (popup != null) // если и тут нету, у нас не вышло его достать
            a(popup, menuItem);
    }
    static void UpdatePopupPlacement(Popup popup, MenuItem menuItem)
    {
        popup.Placement = GetMenuPlacement(menuItem);
    }
    static void UpdatePopupOffset(Popup popup, MenuItem menuItem)
    {
        var offset = GetMenuOffset(menuItem);
        popup.HorizontalOffset = offset.X;
        popup.VerticalOffset = offset.Y;
    }
}

С этим кодом attached property можно навесить не только на «Панно», но и на «Полтавскую баталию».

READ ALSO
Не отрабатывает событие нового кадра NewFrame Aforge

Не отрабатывает событие нового кадра NewFrame Aforge

Я сильно запуталсяWinForms, использую библиотеку Afforge, их пространства имен - AForge

308
Не срабатывает команда OnClientClick

Не срабатывает команда OnClientClick

Всем добрый день! Создаю на форме кнопку через код::

237
HidLibrary чтение нажатия кнопок с клавиатуры

HidLibrary чтение нажатия кнопок с клавиатуры

Консоль закрывается после нажатия любой клавиши, ни как не могу понять где ошибка, подскажите кто работал с этой библиотекой

258
C# Asp.Net Web Forms сделать поиск по сайту

C# Asp.Net Web Forms сделать поиск по сайту

Вся информация с сайта содержится в базе данных XMLКлючевое слово вводится в textbox и при клике на кнопку поиск выпал список данных по ключевому...

260