SpacePurr
@SpacePurr
c#, wpf

Как связать свойство Command Parameter у Context Menu со свойством Name у TextBox, к которому это меню привязано?

Здравствуйте.

Начну значит я с того, что у меня получилось, а потом плавно перейдем к тому, что у меня не получилось.

У меня есть 6 TextBox, которые имеют одинаковое ContextMenu.

<TextBox Grid.Column="2" Grid.Row="5" Text="{Binding TextBoxes[110].BoxValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                    <TextBox.ContextMenu>
                        <ContextMenu ItemsSource="{Binding Companies}" >
                            <ContextMenu.ItemContainerStyle>
                                <Style TargetType="{x:Type MenuItem}">
                                    <Setter Property="ItemsSource" Value="{Binding Workers}"/>
                                    <Setter Property="Header" Value="{Binding CompanyName}"/>
                                    <Setter Property="ItemContainerStyle">
                                        <Setter.Value>
                                            <Style TargetType="{x:Type MenuItem}">
                                                <Setter Property="Header" Value="{Binding Name}"/>
                                                <Setter Property="Command" Value="{Binding MenuCommand}"/>
                                                <Setter Property="CommandParameter" Value="Box_110"/>
                                            </Style>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </ContextMenu.ItemContainerStyle>
                        </ContextMenu>
                    </TextBox.ContextMenu>
                </TextBox>

Обратите внимание на свойство CommandParameter, это единственное свойство, которое уникально у каждого ContextMenu.

Само меню заполняется из ObservableCollection <Company> Companies, расположенном в ViewModel. Коллекция в свою очередь заполняется из небольшого XML файла.

Класс Company имеет поле, содержащее имя компании и список рабочих, которые в свою очередь имеют имя и подпись.
class Company 
    {
        public string CompanyName { get; set; }
        public ObservableCollection<Worker> Workers {get; set;}

        public Company(string displayName)
        {
            CompanyName = displayName;
            Workers = new ObservableCollection<Worker>();
        }


        public class Worker 
        {
            public string Name { get; set; }
            public string Signature { get; set; }

            public ICommand MenuCommand { get; set; }

            public Worker(string name, string signature)
            {
                MenuCommand = new Command(ContextMenuClick, CanExecuteMethod);
                Name = name;
                Signature = signature;
            }

            public void ContextMenuClick(object parameter)
            {
                switch (parameter)
                {                
                    case "Box_115":
                        MessageBox.Show("Ячейка номер 115");
                        StampDictionary.TextBoxes["115"].BoxValue = Name;
                        StampDictionary.TextBoxes["125"].BoxValue = Signature;

                        break;

                    case "Box_114":
                        MessageBox.Show("Ячейка номер 114");
                        StampDictionary.TextBoxes["114"].BoxValue = Name;
                        StampDictionary.TextBoxes["124"].BoxValue = Signature;
                        break;

                    case "Box_113":
                        MessageBox.Show("Ячейка номер 113");
                        StampDictionary.TextBoxes["113"].BoxValue = Name;
                        StampDictionary.TextBoxes["123"].BoxValue = Signature;
                        break;

                    case "Box_112":
                        MessageBox.Show("Ячейка номер 112");
                        StampDictionary.TextBoxes["112"].BoxValue = Name;
                        StampDictionary.TextBoxes["122"].BoxValue = Signature;
                        break;

                    case "Box_111":
                        MessageBox.Show("Ячейка номер 111");
                        StampDictionary.TextBoxes["111"].BoxValue = Name;
                        StampDictionary.TextBoxes["121"].BoxValue = Signature;
                        break;

                    case "Box_110":
                        MessageBox.Show("Ячейка номер 110");
                        StampDictionary.TextBoxes["110"].BoxValue = Name;
                        StampDictionary.TextBoxes["120"].BoxValue = Signature;
                        break;
                }
            }

            public bool CanExecuteMethod(object parameter)
            {
                return true;
            }
        }
    }


Вложенный класс Worker имеет MenuCommand привязанную к ContextMenu
<Setter Property="Command" Value="{Binding MenuCommand}"/>
.

С помощью оператора switch(parameter), я проверяю CommandParameter ContextMenu у TextBox, в котором оно вызвано и выполняю вставку Name и Signature в Dictionary, который через Binding связан с моим TextBox. (Такая махинация со словарем мне нужна для дальнейших действий со вставкой значений в штамп чертежа через Kompas.Api)

Все работает прекрасно. 5ca6290cce0a6876888469.png
5ca629a7b0102750030270.png5ca629e462b4c988863279.png

Однако вот проблема меня задела. Код ContextMenu повторяется целых 6 раз(а могло быть и больше, например), а изменяется только один CommandParameter и я твердо решил постараться изменить такое положение вещей.

ContextMenu я положил в Resources
<Page.Resources>
        <ContextMenu x:Key="MyContexMenu" ItemsSource="{Binding Companies}" >
            <ContextMenu.ItemContainerStyle>
                <Style TargetType="{x:Type MenuItem}">
                    <Setter Property="ItemsSource" Value="{Binding Workers}"/>
                    <Setter Property="Header" Value="{Binding CompanyName}"/>
                    <Setter Property="ItemContainerStyle">
                        <Setter.Value>
                            <Style TargetType="{x:Type MenuItem}">
                                <Setter Property="Header" Value="{Binding Name}"/>
                                <Setter Property="Command" Value="{Binding MenuCommand}"/>
                                <Setter Property="CommandParameter" Value="{Binding Path=Name, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TextBox}}}"/>
                            </Style>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ContextMenu.ItemContainerStyle>
        </ContextMenu>
       ...
</Page.Resources>


Обратите внимание на строку CommandParameter. Я (даже не знаю как я смог такое отрыть) здесь пытаюсь привязать CommandParameter к свойству Name у TextBox, которому принадлежит ContextMenu.
TextBox в свою очередь выглядит так
<TextBox Name="Box_110" Grid.Column="2" Grid.Row="5" Text="{Binding TextBoxes[110].BoxValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                         ContextMenu="{StaticResource MyContexMenu}">


Здесь мы видим, что через StaticResources я подтягиваю ContextMenu и видим Name у TextBox, которое является аналогом того, что раньше было у CommandParameter.

И так, теперь посмотрим скрины, метод работает, но не совсем так как хотелось бы.
5ca62cc50d8e8989730061.png
5ca62d4ea9783611486520.png
5ca62d77eabb2584385982.png

И вроде бы все хорошо да? Прямо как и раньше.
Однако если я постараюсь в другую ячейку выбрать имя из, например Люди, то он посчитает, что ContextMenu вызвано у ячейки Box_110, в котором оно было вызвано в первый раз.
5ca62dd279896976256324.png
5ca62dee28ae1666126753.png

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

Так как же мне решить эту проблему?
Опыта программирования на c# у меня около двух с половиной месяцев, это мой первый большой проект и, наверное, мою реализацию больше можно назвать "творчеством". Однако я точно хочу заставить работать ContextMenu через StaticRecources, потому что мне кажется, что мой алгоритм...нормальный)
Приму к сведению любые махи рукой в сторону, куда копать.

Спасибо.
  • Вопрос задан
  • 1504 просмотра
Решения вопроса 1
FoggyFinder
@FoggyFinder
Вы столкнулись с широко известной проблемой привязки в ContextMenu, ToolTip из-за того что эти элементы не являются частью "визуального" дерева (visual tree).

В таких случаях предлагается два стандартных решениях - использовать PlacementTarget и прокси-объект. Первое тут должно подойти идеально.

Прежде чем показать код отмечу, что вместо использования свойства Name в качестве ключа TextBox лучше использовать свойство Tag.

То есть вместо:

<TextBox Name="Box_110" ...>

<TextBox Tag="Box_110" ...>

Это не принципиальное изменение, но в большинстве ответов для подобных случаев вы встретите использование именно свойство Tag.

С учетом этого привязка для CommandParameter будет выглядеть вот так:

<Setter Property="CommandParameter" Value="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}" />


И немного оффтопа:

В вашем обработчике ContextMenuClick вы используете постоянное смещение равное 10 и код по сути для каждого кейса одинаков. Вы могли бы сократить код, предварительно заменив подсказку в Tag убрав оттуда префикс "Box_" чтобы получилось что-то подобное:

public void ContextMenuClick(object param)
{
    if (int.TryParse(Convert.ToString(param), out int v))
    {
        StampDictionary.TextBoxes[v.ToString()].BoxValue = Name;
        StampDictionary.TextBoxes[(v + 10).ToString()].BoxValue = Signature;
    }
}


Если все ключи в словаре числовые, то еще проще будет заменит тип ключа на int.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы