Простое пользовательское событие
Я пытаюсь изучить пользовательские события, и я пытался создать одно, но кажется, что у меня есть проблема
Я создал форму, статический класс и пользовательское событие. Я пытаюсь добиться того, чтобы при нажатии кнопки Form вызывалась функция статического класса, а затем func время от времени вызывал событие, сообщая о текущем состоянии. Form1 будет прослушивать, если событие вызвано, и если это так, он изменит текст label1
Вот что у меня так далеко
public partial class Form1 : Form
{
public EventHandler<Progress> progress;
public Form1()
{
InitializeComponent();
progress += SetStatus;
}
private void SetStatus(object sender, Progress e)
{
label1.Text = e.Status;
}
private void button1_Click_1(object sender, EventArgs e)
{
TestClass.Func();
}
}
Файл 2
class TestClass
{
public static void Func()
{
//time consuming code
Report status
// time consuming code
report status
}
}
public class Progress : EventArgs
{
public string Status { get; private set; }
private Progress() {}
public Progress(string status)
{
Status = status;
}
}
Теперь я не понимаю, как я могу вызвать событие из TestClass, чтобы Form1 мог обработать событие и изменить метку. Текст
5 ответов
Это простой способ создавать пользовательские события и вызывать их. Вы создаете делегата и событие в классе, из которого вы бросаете. Затем подпишитесь на событие из другой части вашего кода. У вас уже есть собственный класс аргумента события, поэтому вы можете использовать его для создания других классов аргумента события. NB: я не скомпилировал этот код.
public partial class Form1 : Form
{
private TestClass _testClass;
public Form1()
{
InitializeComponent();
_testClass = new TestClass();
_testClass.OnUpdateStatus += new TestClass.StatusUpdateHandler(UpdateStatus);
}
private void UpdateStatus(object sender, ProgressEventArgs e)
{
SetStatus(e.Status);
}
private void SetStatus(string status)
{
label1.Text = status;
}
private void button1_Click_1(object sender, EventArgs e)
{
TestClass.Func();
}
}
public class TestClass
{
public delegate void StatusUpdateHandler(object sender, ProgressEventArgs e);
public event StatusUpdateHandler OnUpdateStatus;
public static void Func()
{
//time consuming code
UpdateStatus(status);
// time consuming code
UpdateStatus(status);
}
private void UpdateStatus(string status)
{
// Make sure someone is listening to event
if (OnUpdateStatus == null) return;
ProgressEventArgs args = new ProgressEventArgs(status);
OnUpdateStatus(this, args);
}
}
public class ProgressEventArgs : EventArgs
{
public string Status { get; private set; }
public ProgressEventArgs(string status)
{
Status = status;
}
}
Вы не создали событие. Для этого напишите:
public event EventHandler<Progress> Progress;
Затем вы можете позвонить Progress
из класса, где он был объявлен как обычная функция или делегат:
Progress(this, new Progress("some status"));
Итак, если вы хотите сообщить о прогрессе в TestClass
событие также должно быть там, и оно должно быть статичным. Вы можете подписаться на него из вашей формы следующим образом:
TestClass.Progress += SetStatus;
Кроме того, вы, вероятно, должны переименовать Progress
в ProgressEventArgs
, так что понятно, что это такое.
События довольно просты в C#, но документы MSDN, на мой взгляд, делают их довольно запутанными. Обычно большая часть документации, которую вы видите, обсуждает создание класса, наследуемого от EventArgs
базовый класс и есть причина для этого. Тем не менее, это не самый простой способ создавать события, и для кого-то, желающего чего-то быстрого и легкого, и в сжатые сроки, используя Action
тип твой билет.
Создание событий и подписка на них
1. Создайте свое мероприятие в своем классе сразу после class
декларация.
public event Action<string,string,string,string>MyEvent;
2. Создайте свой метод класса обработчика событий в вашем классе.
private void MyEventHandler(string s1,string s2,string s3,string s4)
{
Console.WriteLine("{0} {1} {2} {3}",s1,s2,s3,s4);
}
3. Теперь, когда ваш класс вызывается, скажите ему, чтобы связать событие с вашим новым обработчиком событий. Причина +=
оператор используется потому, что вы добавляете к событию ваш конкретный обработчик события. На самом деле вы можете сделать это с несколькими отдельными обработчиками событий, и когда событие возникает, каждый обработчик событий будет работать в той последовательности, в которой вы их добавили.
class Example
{
public Example() // I'm a C# style class constructor
{
MyEvent += new Action<string,string,string,string>(MyEventHandler);
}
}
4. Теперь, когда вы будете готовы, вызовите (или поднимите) событие где-нибудь в коде вашего класса следующим образом:
MyEvent("wow","this","is","cool");
Конечным результатом при запуске является то, что консоль будет выдавать "вау, это круто". И если вы изменили "круто" с датой или последовательностью и запустили этот триггер события несколько раз, вы увидите, что результат получается в последовательности FIFO, как обычно должно происходить событие.
В этом примере я передал 4 строки. Но вы можете изменить их на любой приемлемый тип, или использовать более или менее типы, или даже удалить <...>
и ничего не передавать вашему обработчику событий.
И, опять же, если у вас было несколько пользовательских обработчиков событий, и вы подписали их всех на ваше событие с помощью +=
оператор, то ваш триггер события вызвал бы их всех по порядку.
Выявление участников событий
Но что, если вы хотите идентифицировать абонента этого события в вашем обработчике событий? Это полезно, если вы хотите обработчик события, который реагирует с условиями, основанными на том, кто вызвал / вызвал событие. Есть несколько способов сделать это. Ниже приведены примеры того, как быстро они работают:
Вариант 1. (Самый быстрый) Если вы уже знаете его, передайте имя в виде литеральной строки обработчику событий при его запуске.
Вариант 2. (Довольно быстро) Добавьте это в свой класс и вызовите его из вызывающего метода, а затем передайте эту строку в обработчик события при его запуске:
private static string GetCaller([System.Runtime.CompilerServices.CallerMemberName] string s = null) => s;
Вариант 3. (Наименее быстрый, но все еще быстрый). В вашем обработчике событий, когда вы его запускаете, получите строку имени вызывающего метода с этим:
string callingMethod = new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().ReflectedType.Name.Split('<', '>')[1];
Отписаться от событий
У вас может быть сценарий, в котором ваше пользовательское событие имеет несколько обработчиков событий, но вы хотите удалить один специальный из списка обработчиков событий. Для этого используйте -=
Оператор вроде так:
MyEvent -= MyEventHandler;
Однако, слово малой осторожности с этим. Если вы сделаете это, и у этого события больше не будет никаких обработчиков событий, и вы снова вызовете это событие, оно выдаст исключение. (Исключения, конечно, вы можете ловить с помощью блоков try/catch.)
Очистка всех событий
Хорошо, допустим, вы прошли через события и больше не хотите обрабатывать. Просто установите его в null, вот так:
MyEvent = null;
То же самое предостережение для Отписавшихся событий также здесь. Если в вашем пользовательском обработчике событий больше нет событий, и вы запускаете его снова, ваша программа выдаст исключение.
Как уже упоминалось, поле прогресса требует ключевое слово event
public event EventHandler<Progress> progress;
Но я не думаю, что именно там вы на самом деле хотите ваше мероприятие. Я думаю, что вы действительно хотите событие в TestClass
, Как выглядит следующее? (Я никогда не пробовал настраивать статические события, поэтому я не уверен, скомпилируется ли следующее или нет, но я думаю, что это дает вам представление о шаблоне, к которому вы должны стремиться.)
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
TestClass.progress += SetStatus;
}
private void SetStatus(object sender, Progress e)
{
label1.Text = e.Status;
}
private void button1_Click_1(object sender, EventArgs e)
{
TestClass.Func();
}
}
public class TestClass
{
public static event EventHandler<Progress> progress;
public static void Func()
{
//time consuming code
OnProgress(new Progress("current status"));
// time consuming code
OnProgress(new Progress("some new status"));
}
private static void OnProgress(EventArgs e)
{
if (progress != null)
progress(this, e);
}
}
public class Progress : EventArgs
{
public string Status { get; private set; }
private Progress() {}
public Progress(string status)
{
Status = status;
}
}
Я потратил довольно много времени на изучение того, как добиться этого для своих целей, и нашел ответы, разбросанные по множеству источников. Я постараюсь применить то, что я узнал, к вопросу ФП, а затем расширить его, чтобы удовлетворить асинхронные требования.
Ответ на исходный вопрос
- Как предлагали другие, переместите
EventHandler
к ...
public class TestClass
{
public event EventHandler<Progress> progress;
...
}
...затем создайте экземпляр in и подпишитесь на его событие...
public partial class Form1 : Form
{
private TestClass _testClass;
public Form1()
{
InitializeComponent();
_testClass = new TestClass();
_testClass.progress += SetStatus;
}
...
}
- Измените ссылку на внутренний объект вместо метода...
private void button1_Click_1(object sender, EventArgs e)
{
_testClass.Func();
}
Убрать все
static
модификаторы из методов и свойствTestClass
.Измените метод, включив в него действительный код для тестирования...
public void Func()
{
for (var i = 1; i < 6; i++)
{
System.Threading.Thread.Sleep(2000); // Sleep for 2 seconds
progress?.Invoke(this, new Progress("Iteration " + i + " complete..."));
}
}
ПРИМЕЧАНИЕ. В C#6 добавлен
?
токен, поэтому нет необходимости проверять, есть ли подписчики на событие. Также нет необходимости создавать делегат, что усложняет код. Вы можете просто сообщить об этом событии всем подписчикам.
Это заставит вашу форму работать. Однако он будет заблокирован, поэтому единственным обновлением статуса, которое вы увидите, будет последнее сообщение о статусе. Я предполагаю, что ОП хотел, чтобы форма оставалась разблокированной, чтобы пользователю отображались периодические обновления статуса.
Асинхронное выполнение
Если вы действительно хотите работать в фоновом режиме, вам нужно выполнить этот вызов асинхронно. Для этого выполните следующие дополнительные шаги.
- Добавить
async
модификатор дляbutton_Click_1
и позвоните с помощьюTask.Run
...
private async void button1_Click_1(object sender, EventArgs e)
{
await Task.Run(() =>
{
_testClass.Func();
});
}
- Поскольку событие будет вызвано фоновой задачей, вам необходимо изменить
SetStatus()
кInvoke
установкаlabel1.Text
в потоке, которому принадлежит базовый дескриптор окна элемента управления.
private void SetStatus(object sender, Progress e)
{
if (label1.InvokeRequired)
{
label1.Invoke(new MethodInvoker(() =>
{
label1.Text = e.Status;
}));
} else
{
label1.Text = e.Status;
}
}
Теперь, когда форма выполняется асинхронно, вы заметите, что можете изменять ее размер и взаимодействовать с формой во время ее выполнения в фоновом режиме. Это означает, что вы также можете снова нажать кнопку 1 и запустить второй экземпляр в фоновом режиме.
Чтобы предотвратить это, вы можете изменить , чтобы отключить кнопку до завершения. Я решил сохранить текст кнопки и изменить его на «Обработка...», а затем восстановить исходный текст кнопки, когда закончите.
private async void button1_Click_1(object sender, EventArgs e)
{
var savedText = button1.Text;
button1.Enabled = false;
button1.Text = "Processing...";
await Task.Run(() =>
{
_testClass.Func();
});
button1.Text = savedText;
button1.Enabled = true;
}
Отмена асинхронной задачи
Теперь, когда ваша задача работает в фоновом режиме, вы можете обнаружить, что вам необходимо отменить эту фоновую задачу. Для этого вам необходимо передать токен отмены в метод и периодически проверять его, преждевременно выходя из метода по запросу.
- Создайте частный токен отмены в
Form1
...
public partial class Form1 : Form
{
private CancellationTokenSource funcCancellationToken = new CancellationTokenSource();
...
}
- Передайте новый токен отмены в , каждый раз при его вызове.
funcCancellationToken = new CancellationTokenSource();
await Task.Run(() =>
{
_testClass.Func(funcCancellationToken);
});
- периодически проверяйте токен в течение
Func()
, чтобы определить, следует ли прервать процесс.
public void Func(CancellationTokenSource cancelToken)
{
for (var i = 1; i < 6; i++)
{
if (cancelToken.IsCancellationRequested)
{
progress?.Invoke(this, new Progress("Process canceled after " + (i - 1) + " iterations"));
break;
}
System.Threading.Thread.Sleep(2000); // Sleep for 2 seconds
progress?.Invoke(this, new Progress("Iteration " + i + " complete..."));
}
}
ПРИМЕЧАНИЕ. Существует также
TaskCanceledException()
что можно кинуть, то поймать из , если хотите более подробную информацию о состоянии процесса на момент его отмены.
- Выберите способ, с помощью которого пользователь может запросить отмену фоновой задачи. Для этого примера я решил использовать
button1
для запуска и отмены фоновой задачи. Это делает код немного сложнее, но пользовательский опыт лучше (на мой взгляд). Я изменил следующим образом...
private async void button1_Click_1(object sender, EventArgs e)
{
string savedText = button1.Text;
if (button1.Text == "Cancel")
{
button1.Text = "Cancelling...";
button1.Enabled = false;
funcCancellationToken.Cancel();
} else
{
savedText = button1.Text;
button1.Text = "Cancel";
funcCancellationToken = new CancellationTokenSource();
await Task.Run(() =>
{
_testClass.Func(funcCancellationToken);
});
button1.Enabled = true;
button1.Text = savedText;
}
}
Конечный результат
Теперь, когда я объяснил итерационный процесс, который использовал для получения окончательного результата, которым я был доволен в своем собственном проекте, я предоставлю полный исходный код окончательной асинхронной реализации. Обратите внимание, что я переименовалProgress
введите, чтобыProgressEventArgs
,progress
событие дляOnProgressUpdated
, иbutton1_Click_1
кbutton1_Click_1Async
, чтобы привести код в соответствие с общепринятыми соглашениями об именах.
public partial class Form1 : Form
{
private CancellationTokenSource funcCancellationToken;
private TestClass _testClass;
public Form1()
{
InitializeComponent();
_testClass = new TestClass();
_testClass.OnProgressUpdated += SetStatus;
}
private void SetStatus(object sender, ProgressEventArgs e)
{
if (label1.InvokeRequired)
{
label1.Invoke(new MethodInvoker(() =>
{
label1.Text = e.Status;
}));
} else
{
label1.Text = e.Status;
}
}
private async void button1_Click_1Async(object sender, EventArgs e)
{
string savedText = button1.Text;
if (button1.Text == "Cancel")
{
button1.Text = "Cancelling...";
button1.Enabled = false;
funcCancellationToken.Cancel();
} else
{
savedText = button1.Text;
button1.Text = "Cancel";
funcCancellationToken = new CancellationTokenSource();
await Task.Run(() =>
{
_testClass.Func(funcCancellationToken);
});
button1.Enabled = true;
button1.Text = savedText;
}
}
}
public class TestClass
{
public event EventHandler<ProgressEventArgs> OnProgressUpdated;
public void Func(CancellationTokenSource cancelToken)
{
for (var i = 1; i < 6; i++)
{
if (cancelToken.IsCancellationRequested)
{
OnProgressUpdated?.Invoke(this, new ProgressEventArgs("Process canceled after " + (i - 1) + " iterations"));
break;
}
System.Threading.Thread.Sleep(2000); // Sleep for 2 seconds
OnProgressUpdated?.Invoke(this, new ProgressEventArgs("Iteration " + i + " complete..."));
}
}
}
public class ProgressEventArgs : EventArgs
{
public string Status { get; private set; }
private ProgressEventArgs() { }
public ProgressEventArgs(string status)
{
Status = status;
}
}