Log WinForm按钮单击*之前*由Designer生成的触发事件

时间:2015-01-09 03:32:11

标签: c# .net winforms events delegates

我试图想办法让每个按钮点击一个文件,让我的WinForms应用程序记录下来。到目前为止,我发现的最佳解决方案(来自Log all button clicks in Win Forms app)类似于:

public class ButtonLogger
{
    public static void AttachButtonLogging(ControlCollection controls)
    {
        foreach (var control in controls.Cast<Control>())
        {
            if (control is Button)
            {
                Button button = (Button)control;
                button.Click += LogButtonClick;
            }
            else
            {
                AttachButtonLogging(control.Controls);
            }
        }
    }

    private static void LogButtonClick(object sender, EventArgs eventArgs)
    {
        Button button = sender as Button;
        WriteLog("Click: " + button.Parent.Name.ToString() + "." + button.Name.ToString() + " (\"" + button.Text + "\")");
    }

    private static void WriteLog(string message)
    {
        //...
    }
}

然后在每个表单构造函数的末尾:

ButtonLogger.AttachButtonLogging(this.Controls);

虽然这是一个很好的方法,但它有三个主要缺点,这都是由于日志事件总是发生在&#34;真正的&#34;之后。事件(由Designer创建):

1)如果单击按钮打开模式对话框,则在父对话单击之前将记录该对话框中的单击(也就是日志显示为无序)。这是因为只有在关闭模态对话框后才能记录父母的点击,因此在该对话框中点击后就会记录。

2)同样,如果该模态对话框中的某些操作导致应用程序崩溃,则永远不会记录打开它的点击,使其看起来好像在父级中发生了崩溃。

3)如果按钮单击关闭当前对话框,则button.Parent将为null(因为对话框已被&#34关闭后,记录它的事件将被触发;第一个&#34;事件)。因此,我们无法记录表格的名称,从中&#34;关闭&#34;点击来了。

我一直绞尽脑汁想要扭转秩序(所以日志事件会在#34;真实&#34;事件发生之前被解雇),但是不能提出任何这并不妨碍使用Designer 。问题是设计师既创造了&amp;在InitializeComponents()中分配事件,我们无法编辑。

关于可能(不成功)解决方案的想法:

  • 从Designer在按钮创建和添加事件之间放置的东西调用AttachButtonLogging()。但是,唯一有一个调用SuspendLayout()的东西,它不是虚拟的,因此不能被覆盖(并且那时按钮还没有被添加到ControlCollection中)。 / p>

  • 让AttachButtonLogging()移除每个按钮的现有事件,添加日志事件,然后重新添加原件。但是,您无法从该课程外部枚举班级活动(即,附加按钮记录无法枚举按钮的活动)。

看起来这样的事情应该可以通过一些开箱即用的思考来实现,但我的想法很新鲜。注意:目标是在构建新对话框时以非繁琐的方式记录点击次数 - 类似于上面的解决方案,但没有提到的三个陷阱。

3 个答案:

答案 0 :(得分:2)

只需使用IMessageFilter()Application.AddMessageFilter()一起获取之前的消息即可将其发送到您的应用。这适用于所有形式的应用程序中的所有按钮(无论它们嵌套多深)。它甚至适用于在运行时动态添加的按钮。 最棒的是,您根本不需要使用现有代码和控件进行任何更改。您只需添加消息过滤器一次启动表单的Load()事件:

public partial class Form1 : Form
{

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        Application.AddMessageFilter(new ButtonLogger());
    }

}

public class ButtonLogger : IMessageFilter
{

    private const int WM_KEYUP = 0x101;
    private const int WM_LBUTTONUP = 0x202;

    public bool PreFilterMessage(ref Message m)
    {
        if (m.Msg == WM_LBUTTONUP || (m.Msg == WM_KEYUP && ((int)m.WParam == 32 || (int)m.WParam == 13)))
        {
            Control ctl = Control.FromHandle(m.HWnd);
            if (ctl is Button)
            {
                LogButtonClick((Button)ctl);
            }
        }
        return false; // allow normal processing of all messages
    }

    private void LogButtonClick(Button btn)
    {
        WriteLog("Click: " + btn.Parent.Name.ToString() + "." + btn.Name.ToString() + " (\"" + btn.Text + "\")");
    }

    private void WriteLog(string message)
    {
        Console.WriteLine(message);
    }

}

答案 1 :(得分:1)

这里有很多选择,有些比其他选项更令人发指。 :)不幸的是,一般来说,解决方案越方便,就越令人发指。

选项1:(至少令人发指)

不要使用Designer(我们称之为&#34; GUI构建器&#34;在Visual Studio中)来附加事件处理程序。相反,编写代码以自己附加每个按钮的处理程序,使用单击记录器中的辅助方法自动插入所需的处理程序:

class LogClickEventArgs : EventArgs
{
    public string Name { get; private set; }
    public DateTime DateTime { get; private set; }

    public LogClickEventArgs(string name)
    {
        Name = name;
        DateTime = DateTime.UtcNow;
    }
}

class ClickLogger
{
    public static event EventHandler<LogClickEventArgs> LogClick;

    public static void SubscribeClick(Control control, EventHandler handler)
    {
        control.Click += (_ClickHandler + handler);
    }

    private static void _ClickHandler(object sender, EventArgs e)
    {
        LogClick.Raise(null, new LogClickEventArgs(((Control)sender).Name));
    }
}

static class Extensions
{
    public static void Raise<T>(this EventHandler<T> handler, object sender, T e) where T : EventArgs
    {
        if (handler != null)
        {
            handler(sender, e);
        }
    }
}

Form子类中:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        ClickLogger.SubscribeClick(button1, button_Click);
        ClickLogger.SubscribeClick(button2, button_Click);
        ClickLogger.SubscribeClick(button3, button_Click);

        ClickLogger.LogClick += (sender, e) =>
        {
            textBox1.AppendText(string.Format("{0} -- {1}\r\n", e.DateTime, e.Name));
        };
    }

    private void button_Click(object sender, EventArgs e)
    {
        textBox1.AppendText(string.Format("==> clicked {0}\r\n", ((Control)sender).Name));
    }
}

恕我直言,设计师事件订阅用户界面(双击属性窗口中的事件来创建和订阅处理程序)并不是所有 方便。上述方法不是在Designer中双击,而是在Form构造函数中添加适当的代码行。

选项2:

使用自定义Button子类,您可以在其中覆盖OnClick()方法并记录点击:

class ClickLogger
{
    public static event EventHandler<LogClickEventArgs> LogClick;

    public static void NotifyClick(Control control)
    {
        LogClick.Raise(null, new LogClickEventArgs(control.Name));
    }
}

自定义Button子类:

public partial class LoggingButton : Button
{
    public LoggingButton()
    {
        InitializeComponent();
    }

    protected override void OnClick(EventArgs e)
    {
        ClickLogger.NotifyClick(this);
        base.OnClick(e);
    }
}

恕我直言,这是相当方便的,但当然确实有一个缺点,就是你必须要为你要记录Control事件的任何Click类进行子类化。您可以像在任何其他LoggingButton中一样在Designer中操作Control类,并且使用自定义替换Button文件中的现有*.Designer.cs实例是微不足道的一个(易于搜索和替换)。

选项3:(最令人发指)

作弊,并使用反射来访问Control类的私有实现细节,允许自己直接操作底层数据结构来插入处理程序。这是您现在使用的技术的一种变体,但修复了您自己的日志处理程序最终调用的调用列表的哪一端的问题:

class ClickLogger
{
    public static event EventHandler<LogClickEventArgs> LogClick;

    public static void AttachLogging<T>(ControlCollection controls) where T : Control
    {
        foreach (Control control in controls)
        {
            if (control is T)
            {
                _AttachLoggingToControl(control);
            }

            AttachLogging<T>(control.Controls);
        }
    }

    private static void _AttachLoggingToControl(Control control)
    {
        FieldInfo fi = typeof(Control).GetField("EventClick", BindingFlags.Static | BindingFlags.NonPublic);
        PropertyInfo pi = typeof(Control).GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic);

        object eventClickObject = fi.GetValue(null);
        EventHandlerList handlerList = (EventHandlerList)pi.GetValue(control);
        EventHandler clickHandlers = (EventHandler)handlerList[eventClickObject];

        handlerList[eventClickObject] = _ClickHandler + clickHandlers;
    }

    private static void _ClickHandler(object sender, EventArgs e)
    {
        LogClick.Raise(null, new LogClickEventArgs(((Control)sender).Name));
    }
}

Form子类中:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        ClickLogger.AttachLogging<Button>(Controls);

        ClickLogger.LogClick += (sender, e) =>
        {
            textBox1.AppendText(string.Format("{0} -- {1}\r\n", e.DateTime, e.Name));
        };
    }
}

这是你最接近你要做的事情。但是,它在理论上非常脆弱,因为它依赖于未记录的实现细节,并且不一定是固定的。

一个主要的缓解因素是System.Windows.Forms命名空间正式处于&#34;仅错误修复&#34;模式。此外,微软已开源代码,虽然无法保证实施不会发生变化,但这意味着更多人会开始做这样的令人发指的事情,增加了微软改变实施的不情愿。在任何情况下,如果他们确实改变了实现,因为它是开源的,因此修复代码以解决这个问题将会非常令人头痛。


甚至还有其他选择。但我认为以上是平衡便利与危害之间的最佳选择。 :)


修改

我还有另一种选择。我觉得你有足够的想法,所以我不会完全把它弄出来,但我会提到它,以防你有特别的想法...

选项4:

您可以考虑查看记录器实现本身,而不是关注日志记录客户端代码。例如,您可以设置消息依赖关系,其中某些消息被视为&#34;依赖&#34;在其他消息或消息类别/级别上,并且在其中一个消息本身被记录之前不会记录。

更具体地说:您可以根据记录Click事件本身的消息对在处理Click事件期间记录的消息进行分类。打电话给前者&#34; 1级&#34;消息,后者是&#34;等级0&#34;信息。实现日志记录代码,以便简单地批量处理&#34;级别1&#34;消息,直到它收到&#34;等级0&#34;消息,此时它首先记录&#34;级别0&#34;消息然后排队等级1&#34;消息。


最后...... (我保证,关于这个问题的最后一句话)

我真的应该在一开始就提到这一点,但当然你的一个选择就是不要担心这一点。特别是,我不清楚为什么Click事件本身确实需要记录,以及为什么在实际处理Click事件期间记录的消息不是本身足以告知读者Click事件发生的日志。

换句话说,另一种选择是不记录Click事件。这显然是最简单和最容易实现的,并且最不可能在将来需要维护。 :)

答案 2 :(得分:0)

我想我(终于)想出了一个解决方案(下面是解释)。首先,记录器变为:

public class ButtonLogger
{
    public static void AttachButtonLogging(Form form)
    {
        foreach (var field in form.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
        {
            if (field.GetValue(form) is Button)
            {
                System.Windows.Forms.Button button = (Button)field.GetValue(form);
                button.Click += LogButtonClick;
            }
        }
    }

    private static void LogButtonClick(object sender, EventArgs eventArgs)
    {
        Button button = sender as Button;
        WriteLog("Click: " + button.Parent.Name.ToString() + "." + button.Name.ToString() + " (\"" + button.Text + "\")");
    }

    private static void WriteLog(string message)
    {
        //...
    }
}

然后我们在每种形式中执行以下操作:

public new void SuspendLayout()
{
    base.SuspendLayout();
    ButtonLogger.AttachButtonLogging(this);
}

逻辑是Designer在创建每个按钮后插入对SuspendLayout()的调用,但是在向按钮添加事件之前 - 因此,如果我们此时可以添加我们的日志事件,它们将在设计者之前被触发#39事件。但是,此时按钮尚未添加到表单的控件列表中。因此,我们使用反射来检查所有表单的字段,并且对于每个表单的按钮,我们添加我们的事件。

需要进行更多测试,但就目前而言,它似乎运行良好!