Home > OS >  Is there a way to implement a custom context menu for the Winforms reportViewer control?
Is there a way to implement a custom context menu for the Winforms reportViewer control?

Time:03-26

I'm trying to implement a custom context menu for a WinForms.ReportViewer control. I can get my custom menu to show if the user right clicks in the "toolbar" area of the ReportViewer control at the very top of the control, but when clicking in the main part of the control that displays the actual report data, the default context menu that comes with the ReportViewer displays. Is there a way to get my context menu to replace the default?

I've searched through the controls collection of the ReportViewer, none of them are a ContextMenuStrip. If it matters, the ReportViewer control version is 15.0.0.0.

private void CreateContextMenu()
{
    ContextMenuStrip menuStrip = new ContextMenuStrip();
    ToolStripMenuItem menuItem = new ToolStripMenuItem("Exit");

    menuItem.Click  = new EventHandler(MenuItem_Click);
    menuItem.Name = "Exit";
    menuStrip.Items.Add(menuItem);

    // This makes no difference.
    //this.reportViewer1.ContextMenu = null;
    this.reportViewer1.ContextMenuStrip = menuStrip;
}

private void MenuItem_Click(object sender, EventArgs e)
{
    ToolStripItem menuItem = (ToolStripItem)sender;
    if(menuItem.Name == "Exit")
    {
        Application.Exit();
    }
}

CodePudding user response:

Yes you can replace the report's default ContextMenuStrip. You need to get the renderer's container which contains and shows the default CMS. The only control in the ReportViewer controls tree that has a CMS is a nameless control of type ReportPanel. We can use this fact to get that control and replace the CMS.

Solution 1

The target panel doesn't have a name/key to get it directyly by the ControlCollection.Find method. However, its parent does have a name which is winRSviewer. So you can write:

//  
// using System.Linq;
// ...

var c = reportViewer1.Controls.Find("winRSviewer", true)
    .FirstOrDefault()?.Controls.OfType<Control>()
    .FirstOrDefault(x => x.ContextMenuStrip != null);

if (c != null) c.ContextMenuStrip = contextMenuStrip1;

Solution 2

Use a recursive function to get the target control:

private void SomeCaller()
{
    var c = GetChildren(reportViewer1).FirstOrDefault(x => x.ContextMenuStrip != null);

    if (c != null) c.ContextMenuStrip = contextMenuStrip1;
}

private IEnumerable<Control> GetChildren(Control parent) => parent.Controls
    .Cast<Control>().Concat(parent.Controls
    .Cast<Control>().SelectMany(GetChildren));

Solution 3

Create a new class and inherits from the ReportViewer control. Add a property that allows you to switch between the default CMS and one of your choice.

public class ReportViewerEx : ReportViewer
{
    private readonly Control reportPanel;
    private readonly ContextMenuStrip defaultCMS;

    public ReportViewerEx() : base() 
    {
        reportPanel = GetChildren(this).FirstOrDefault(x => x.ContextMenuStrip != null);
        if (reportPanel != null) defaultCMS = reportPanel.ContextMenuStrip;
    }

    private ContextMenuStrip customCMS;
    [DefaultValue(null)]
    public ContextMenuStrip ReportPanelContextMenuStrip
    {
        get => customCMS;
        set
        {
            customCMS = value;
            if (reportPanel != null) reportPanel.ContextMenuStrip = value ?? defaultCMS;
        }
    }

    private IEnumerable<Control> GetChildren(Control parent) => parent.Controls
            .Cast<Control>().Concat(parent.Controls
            .Cast<Control>().SelectMany(GetChildren));
}

Build and drop an instance. Find the ReportPanelContextMenuStrip in the properties window to select a different CMS from the dropdown. Reset the property to use the default CMS. Alternatively, set the property by code. null to reset.

  • Related