Wednesday, April 15, 2020

Dynamically generated and formatted context menus

I have a user who has an interesting enhancement for one of my screens. The main part of the screen is a four level Infragistics datagrid. They want to know if payments buried at the third level have attachments (scanned documents).

So they want a paper clip displayed at the top level, if any of the payments within that row have attachments. They want the paperclip to have a tooltip with clickable links. I explained that's not a viable solution because the tooltip will close as soon as they move the mouse towards it.

I suggested a context sensitive menu, but I was unsure whether I could generate one on the fly (there are different attachments to be displayed for each top level row) and I was also unsure if I could format it nicely (it's just a menu).

I saw the MenuItem.Header property is an object so I should be able to construct some kind of panel and use that as the header. Maybe I can also leverage the ContextMenu.Opened event to regenerate the context menu's contents on the fly.

Here's a greatly simplified solution with just a list of payments and a context sensitive menu listing their attachments.


<Window x:Class="ClickableTooltip.MainWindow"
        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:ClickableTooltip"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="OpenAttachment"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource OpenAttachment}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <DataGrid ItemsSource="{Binding Payments}" IsReadOnly="True" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTemplateColumn>
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image Source="paperclip.jpg" Height="20">
                            <Image.ContextMenu>
                                <ContextMenu Opened="ContextMenu_Opened"/>
                            </Image.ContextMenu>
                        </Image>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTextColumn Binding="{Binding}"/>
        </DataGrid.Columns>
    </DataGrid>
</Window>

I got paperclip.jpg from Google Images. Feel free to choose your own. The jpg file is in the root of the project.

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace ClickableTooltip
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public class cAttachment
        {
            public string PaymentNumber { get; set; }
            public string AttachmentName { get; set; }
            public string Creator { get; set; }
        }
     
        public List<cAttachment> Attachments
        {
            get
            {
                return new List<cAttachment>()
                {
                    new cAttachment() {AttachmentName ="County guidelines", Creator="rsnipper", PaymentNumber="123456"},
                    new cAttachment() {AttachmentName="Vendor contract", Creator="abaglady", PaymentNumber="123478"},
                    new cAttachment() {AttachmentName="A ridiculously long attachment name that goes on forever", Creator="rsmith", PaymentNumber="123478" }
                };
            }
        }

        public List<string> Payments
        {
            get
            {
                return Attachments.Select(a => a.PaymentNumber).Distinct().ToList();
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        private void ContextMenu_Opened(object sender, RoutedEventArgs e)
        {
            ContextMenu cm = sender as ContextMenu;
            RoutedCommand oa = this.FindResource("OpenAttachment") as RoutedCommand;
            cm.Items.Clear();

            foreach (cAttachment a in Attachments.Where(a => a.PaymentNumber==cm.DataContext.ToString()))
            {
                MenuItem mi = new MenuItem();
                StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
                sp.Children.Add(new TextBlock() { Text = a.PaymentNumber, Width = 60 });
                sp.Children.Add(new TextBlock() { Text = a.AttachmentName, TextTrimming=TextTrimming.CharacterEllipsis, Width = 150 });
                sp.Children.Add(new TextBlock() { Text = a.Creator });
                mi.Header = sp;

                mi.Command = oa;
                mi.CommandParameter = a.AttachmentName;
                mi.CommandTarget = cm.PlacementTarget;
                cm.Items.Add(mi);
            }
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show(string.Format("Here is attachment '{0}'", e.Parameter), "Hi");
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

    }
}

The interesting bit is ContextMenu_Opened which grabs a reference to the context menu, clears out it's items, and regenerates them. Note the line mi.CommandTarget = cm.PlacementTarget; which is needed to fix a bug in WPF. Without an explicit command target, the menu items are not selectable.

The result looks like this.


No comments:

Post a Comment