Лучший способ написать метод расширения для вызова элемента управления?

У меня есть эта общая функция для вызова элемента управления WinForm:

public static void Invoke(this Control c, Action action)
{
    if (c.InvokeRequired)
        c.TopLevelControl.Invoke(action);
    else
        action();
}

Я думаю сделать это лучше, введя более жесткие ограничения, чтобы предотвратить бессмысленные вещи, например:

button1.Invoke(() => list.Add(1));

Также может быть избыточная типизация, например:

button1.Invoke(() => button1.Hide());

поскольку мы уже указываем, что this равно button1.

Итак, я сделал это:

public static void Invoke<T>(this T c, Action<T> action) where T : Control
{
    if (c.InvokeRequired)
        c.TopLevelControl.Invoke(action);
    else
        action(c);
}

Теперь мне придется позвонить,

button1.Invoke((c) => c.Hide());

or

button1.Invoke((c) => button1.Hide());

Теперь я чувствую, что даже тогда есть больше, чем требуется. Если я указываю, что this равно button1, то в лямбда-выражении я не хочу снова указывать фиктивную переменную c, чтобы указать, где работать. Могу ли я сделать это снова короче? Возможно, как

button1.Invoke(Hide);

or

button1.Hide.Invoke();

or so in C#?


person nawfal    schedule 24.12.2012    source источник
comment
Также см. stackoverflow.com/questions/2367718 /   -  person nawfal    schedule 27.04.2013


Ответы (5)


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

Теперь ваше первое предложение:

button1.Invoke(Hide);

может работать, если вы сделаете это:

button1.Invoke(button1.Hide); 

потому что иначе компилятор не может знать, где искать метод Hide(). Это может даже вызвать странное поведение, например, если весь этот код находится в каком-то производном классе, например:

class A : Control {
    public A() {
         Button button1=new Button();
         button1.Invoke(Hide);
    }
}

Теперь он будет компилироваться, но метод Hide() будет методом Hide() всего элемента управления, а не кнопки! Способ добиться этого прост:

public static void Invoke(this Control c, Action action) {
    c.Invoke(action);
}

Последний способ:

button1.Hide().Invoke();

будет работать даже без добавления методов расширения, вам просто нужно сделать это:

((Action)button1.Hide).Invoke();

Это, конечно, означает, что метод Hide() вызывается в текущем потоке, что, вероятно, не то, что вам нужно. Так сделайте это:

((Action)button1.Hide).Invoke(button1);
public static void Invoke(this Action action, Control c) {
    c.Invoke(action);
}

Извините за длинный ответ, надеюсь, это поможет.

person cre8or    schedule 24.12.2012
comment
Хороший ответ. Я думал о том, где мне не нужно указывать button1 дважды. - person nawfal; 24.12.2012
comment
Да, я не думаю, что это возможно - просто потому, что вам нужно указать объект для метода Invoke() И объект для метода Hide(). Конечно, вы можете создать класс Button и добавить метод, например InvokeHide(), но, поскольку ваша цель — свести к минимуму набор текста, я не думаю, что это хорошая идея :-) - person cre8or; 24.12.2012

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

public static void Invoke<T>(this T c, Action<T> action) where T : Control
    {
        if (c.InvokeRequired)
            c.Invoke(new Action<T, Action<T>>(Invoke), new object[] { c, action });
        else
            action(c);
    }

Это предотвратит выбрасывание TargetParameterCountException при кросс-поточности.

Звонить:

button1.Invoke(x => x.Hide());
person Sparky    schedule 22.01.2015

Вы можете использовать SynchronizationContext.Post или SynchronizationContext.Send, чтобы платформа упорядочивала действие для поток пользовательского интерфейса, будь то Windows Forms или WPF. Статический метод SynchronizationContext.Current возвращает соответствующий контекст синхронизации для вашего типа приложения.

Post выполняется асинхронно, тогда как Send блокируется до завершения действия.

Следующий код асинхронно скроет кнопку:

SynchronizationContext.Current.Post(_=>button1.Hide(),null);
person Panagiotis Kanavos    schedule 24.12.2012

Я бы пошел с:

public static void Invoke<T>(this T c, Action<T> action) where T : Control
{
    if (c.InvokeRequired)
        c.TopLevelControl.Invoke(action);
    else
        action(c);
}

а также

button.Invoke(c => c.Hide());

Это самый чистый (вам дается первоначально указанная кнопка для выполнения действия) и самый безопасный (вам не нужно указывать button1 дважды... он возвращается вам как параметр вашей лямбда-выражения) ). Я считаю, что это элегантный синтаксис.

person jam40jeff    schedule 25.12.2012

Это определенно нельзя сделать как button1.Invoke(Hide); или button1.Hide.Invoke(); из-за ограничений синтаксиса C#.

Но если вы готовы отказаться от IntelliSense, вы можете сделать его немного короче. Как недостаток, некоторые ошибки, которые обычно можно обнаружить и исправить во время компиляции (например, опечатки или несоответствие параметров), станут ошибками времени выполнения. Иногда это приемлемо, иногда нет.

Забегая вперед, вот пример использования:

button1.Invoke("Hide");

or

button1.Invoke("ResumeLayout", true);

Решение:

internal static class ExtensionMethods
{
    internal static object Invoke<TControl>(this TControl control,
        string methodName, params object[] parameters)
        where TControl : Control
    {
        object result;

        if (control == null)
            throw new ArgumentNullException("control");

        if (string.IsNullOrEmpty(methodName))
            throw new ArgumentNullException("methodName");

        if (control.InvokeRequired)
            result = control.Invoke(new MethodInvoker(() => Invoke(control,
                methodName, parameters)));
        else
        {
            MethodInfo mi = null;

            if (parameters != null && parameters.Length > 0)
            {
                Type[] types = new Type[parameters.Length];
                for (int i = 0; i < parameters.Length; i++)
                {
                    if (parameters[i] != null)
                        types[i] = parameters[i].GetType();
                }

                mi = control.GetType().GetMethod(methodName,
                    BindingFlags.Instance | BindingFlags.Public,
                    null,  types, null);
            }
            else
                mi = control.GetType().GetMethod(methodName,
                    BindingFlags.Instance | BindingFlags.Public);

            if (mi == null)
                throw new InvalidOperationException(methodName);

            result = mi.Invoke(control, parameters);
        }

        return result;
    }
person Nikolay Khil    schedule 24.12.2012
comment
@nawfal, согласен. Но выбирая между ((Action)button1.Hide).Invoke(); и button1.Invoke((c) => c.Hide()); я бы выбрал последний :) - person Nikolay Khil; 24.12.2012
comment
Сделаю так, потому что, в частности, для методов с параметрами @cre8or решение станет ((Action<bool>)button1.ResumeLayout).Invoke(true); против исходного button1.Invoke(c => c.ResumeLayout(true)); - person Nikolay Khil; 24.12.2012
comment
решение cre8or не станет ((Action<bool>)button1.ResumeLayout).Invoke(true);, так как он не рекомендовал его (как он правильно указал, это не позволит избежать ошибки перекрестного потока). Он просто упомянул об этом. - person nawfal; 24.12.2012
comment
@nawfal, так какое он рекомендует решение? Что ж, button1.Invoke(button1.Hide); скомпилируется после небольшого исправления. Но все равно ужасно: button1.Invoke((Action<bool>)button1.ResumeLayout, true);, button1.Invoke((Action<int, int, int, int>)button1.SetBounds, 0, 0, 10, 10);. И это также нарушает безопасность типов. Хотя я согласен, что мое решение еще менее типобезопасно. - person Nikolay Khil; 24.12.2012
comment
Он предложил несколько вариантов, попросив меня выбрать один. Я пошел со своим первым подходом, это не имеет большого значения. Я просто знал об альтернативных лучших ответах, если таковые имеются. Смотрите третий ответ, лучше. - person nawfal; 24.12.2012