diff --git a/docs/csharp/event-pattern.md b/docs/csharp/event-pattern.md index eb1cf724edec9..2605f31fe5559 100644 --- a/docs/csharp/event-pattern.md +++ b/docs/csharp/event-pattern.md @@ -1,7 +1,7 @@ --- title: Standard .NET event patterns description: Learn about .NET event patterns and how to create standard event sources and subscribe and process standard events in your code. -ms.date: 06/20/2016 +ms.date: 09/02/2022 ms.technology: csharp-fundamentals ms.assetid: 8a3133d6-4ef2-46f9-9c8d-a8ea8898e4c9 --- @@ -10,50 +10,29 @@ ms.assetid: 8a3133d6-4ef2-46f9-9c8d-a8ea8898e4c9 [Previous](events-overview.md) -.NET events generally follow a few known patterns. Standardizing -on these patterns means that developers can leverage knowledge of -those standard patterns, which can be applied to any .NET event program. +.NET events generally follow a few known patterns. Standardizing on these patterns means that developers can leverage knowledge of those standard patterns, which can be applied to any .NET event program. -Let's go through these standard patterns so you will have all -the knowledge you need to create standard event sources, and -subscribe and process standard events in your code. +Let's go through these standard patterns so you will have all the knowledge you need to create standard event sources, and subscribe and process standard events in your code. -## Event Delegate Signatures +## Event delegate signatures The standard signature for a .NET event delegate is: ```csharp -void OnEventRaised(object sender, EventArgs args); +void EventRaised(object sender, EventArgs args); ``` -The return type is void. Events are based on delegates and are -multicast delegates. That supports multiple subscribers for any -event source. The single return value from a method doesn't scale -to multiple event subscribers. Which return value does the event -source see after raising an event? Later in this article you'll -see how to create event protocols that support event subscribers -that report information to the event source. - -The argument list contains two arguments: the sender, and the event -arguments. The compile time type of `sender` is `System.Object`, -even though you likely know a more derived type that would always -be correct. By convention, use `object`. - -The second argument has typically been a type that is derived from -`System.EventArgs`. (You'll see in the -[next section](modern-events.md) that this convention is no longer -enforced.) If your event type does not need any additional -arguments, you will still provide both arguments. -There is a special value, `EventArgs.Empty` that you should use to -denote that your event does not contain any additional information. - -Let's build a class that lists files in a directory, or any of its -subdirectories that follow a pattern. This component raises an event -for each file found that matches the pattern. - -Using an event model provides some design advantages. You can create -multiple event listeners that perform different actions when a sought -file is found. Combining the different listeners can create more +The return type is void. Events are based on delegates and are multicast delegates. That supports multiple subscribers for any event source. The single return value from a method doesn't scale to multiple event subscribers. Which return value does the event source see after raising an event? Later in this article you'll +see how to create event protocols that support event subscribers that report information to the event source. + +The argument list contains two arguments: the sender, and the event arguments. The compile-time type of `sender` is `System.Object`, even though you likely know a more derived type that would always be correct. By convention, use `object`. + +The second argument has typically been a type that is derived from `System.EventArgs`. (You'll see in the +[next section](modern-events.md) that this convention is no longer enforced.) If your event type does not need any additional arguments, you will still provide both arguments. There is a special value, `EventArgs.Empty` that you should use to denote that your event does not contain any additional information. + +Let's build a class that lists files in a directory, or any of its subdirectories that follow a pattern. This component raises an event for each file found that matches the pattern. + +Using an event model provides some design advantages. You can create multiple event listeners that perform different actions when a sought file is found. Combining the different listeners can create more robust algorithms. Here is the initial event argument declaration for finding a sought @@ -61,38 +40,21 @@ file: [!code-csharp[EventArgs](../../samples/snippets/csharp/events/Program.cs#EventArgsV1 "Define event arguments")] -Even though this type looks like a small, data-only type, you should -follow the convention and make it a reference (`class`) type. That -means the argument object will be passed by reference, and any -updates to the data will be viewed by all subscribers. The first -version is an immutable object. You should prefer to make the -properties in your event argument type immutable. That way, one -subscriber cannot change the values before another subscriber sees -them. (There are exceptions to this, as you'll see below.) +Even though this type looks like a small, data-only type, you should follow the convention and make it a reference (`class`) type. That means the argument object will be passed by reference, and any updates to the data will be viewed by all subscribers. The first version is an immutable object. You should prefer to make the properties in your event argument type immutable. That way, one subscriber cannot change the values before another subscriber sees them. (There are exceptions to this, as you'll see below.) -Next, we need to create the event declaration in the FileSearcher -class. Leveraging the `EventHandler` type means that you don't -need to create yet another type definition. You simply use a generic -specialization. +Next, we need to create the event declaration in the FileSearcher class. Leveraging the `EventHandler` type means that you don't need to create yet another type definition. You simply use a generic specialization. -Let's fill out the FileSearcher class to search for files that match -a pattern, and raise the correct event when a match is discovered. +Let's fill out the FileSearcher class to search for files that match a pattern, and raise the correct event when a match is discovered. [!code-csharp[FileSearcher](../../samples/snippets/csharp/events/Program.cs#FileSearcherV1 "Create the initial file searcher")] -## Defining and Raising Field-Like Events +## Define and raise field-like events -The simplest way to add an event to your class is to declare that -event as a public field, as in the preceding example: +The simplest way to add an event to your class is to declare that event as a public field, as in the preceding example: [!code-csharp[DeclareEvent](../../samples/snippets/csharp/events/Program.cs#DeclareEvent "Declare the file found event")] -This looks like it's declaring a public field, which would appear to -be bad object-oriented practice. You want to protect data access -through properties, or methods. While this may look like a bad -practice, the code generated by the compiler does create wrappers so -that the event objects can only be accessed in safe ways. The only -operations available on a field-like event are add handler: +This looks like it's declaring a public field, which would appear to be bad object-oriented practice. You want to protect data access through properties, or methods. While this may look like a bad practice, the code generated by the compiler does create wrappers so that the event objects can only be accessed in safe ways. The only operations available on a field-like event are add handler: [!code-csharp[DeclareEventHandler](../../samples/snippets/csharp/events/Program.cs#DeclareEventHandler "Declare the file found event handler")] @@ -100,72 +62,54 @@ and remove handler: [!code-csharp[RemoveEventHandler](../../samples/snippets/csharp/events/Program.cs#RemoveHandler "Remove the event handler")] -Note that there's a local variable for the handler. If you used -the body of the lambda, the remove would not work correctly. It would -be a different instance of the delegate, and silently do nothing. +Note that there's a local variable for the handler. If you used the body of the lambda, the remove would not work correctly. It would be a different instance of the delegate, and silently do nothing. -Code outside the class cannot raise the event, nor can it perform any -other operations. +Code outside the class cannot raise the event, nor can it perform any other operations. -## Returning Values from Event Subscribers +## Return values from event subscribers Your simple version is working fine. Let's add another feature: Cancellation. -When you raise the found event, listeners should be able to stop -further processing, if this file is that last one sought. - -The event handlers do not return a value, so you need to communicate -that in another way. The standard event pattern uses the EventArgs -object to include fields that event subscribers can use to -communicate cancel. - -There are two different patterns that could be used, based on the -semantics of the Cancel contract. In both cases, you'll add a boolean -field to the EventArguments for the found file event. - -One pattern would allow any one subscriber to cancel the operation. -For this pattern, the new field is initialized to `false`. Any -subscriber can change it to `true`. After all subscribers have seen -the event raised, the FileSearcher component examines the boolean -value and takes action. - -The second pattern would only cancel the operation if all subscribers -wanted the operation cancelled. In this pattern, the new field is -initialized to indicate the operation should cancel, and any -subscriber could change it to indicate the operation should continue. -After all subscribers have seen the event raised, the FileSearcher -component examines the boolean and takes action. There is one extra -step in this pattern: the component needs to know if any subscribers -have seen the event. If there are no subscribers, the field would +When you raise the found event, listeners should be able to stop further processing, if this file is the last one sought. + +The event handlers do not return a value, so you need to communicate that in another way. The standard event pattern uses the `EventArgs` object to include fields that event subscribers can use to communicate cancel. + +Two different patterns could be used, based on the semantics of the Cancel contract. In both cases, you'll add a boolean field to the EventArguments for the found file event. + +One pattern would allow any one subscriber to cancel the operation. For this pattern, the new field is initialized to `false`. Any subscriber can change it to `true`. After all subscribers have seen the event raised, the FileSearcher component examines the boolean value and takes action. + +The second pattern would only cancel the operation if all subscribers wanted the operation canceled. In this pattern, the new field is initialized to indicate the operation should cancel, and any subscriber could change it to indicate the operation should continue. After all subscribers have seen the event raised, the FileSearcher component examines the boolean and takes action. There is one extra step in this pattern: the component needs to know if any subscribers have seen the event. If there are no subscribers, the field would indicate a cancel incorrectly. -Let's implement the first version for this sample. You need to add a -boolean field named `CancelRequested` to the `FileFoundArgs` type: +Let's implement the first version for this sample. You need to add a boolean field named `CancelRequested` to the `FileFoundArgs` type: [!code-csharp[EventArgs](../../samples/snippets/csharp/events/Program.cs#EventArgs "Update event arguments")] -This new Field is automatically initialized to `false`, the default value for a Boolean field, so you don't cancel -accidentally. The only other change to the component is to -check the flag after raising the event to see if any of the -subscribers have requested a cancellation: +This new field is automatically initialized to `false`, the default value for a `Boolean` field, so you don't cancel accidentally. The only other change to the component is to check the flag after raising the event to see if any of the subscribers have requested a cancellation: ```csharp -public void List(string directory, string searchPattern) +private void SearchDirectory(string directory, string searchPattern) { foreach (var file in Directory.EnumerateFiles(directory, searchPattern)) { - var args = new FileFoundArgs(file); - FileFound?.Invoke(this, args); + FileFoundArgs args = RaiseFileFound(file); if (args.CancelRequested) + { break; + } } } + +private FileFoundArgs RaiseFileFound(string file) +{ + var args = new FileFoundArgs(file); + FileFound?.Invoke(this, args); + return args; +} ``` -One advantage of this pattern is that it isn't a breaking change. -None of the subscribers requested a cancellation before, and they still are -not. None of the subscriber code needs updating unless they want to +One advantage of this pattern is that it isn't a breaking change. None of the subscribers requested cancellation before, and they still are not. None of the subscriber code needs updating unless they want to support the new cancel protocol. It's very loosely coupled. Let's update the subscriber so that it requests a cancellation once @@ -179,66 +123,44 @@ EventHandler onFileFound = (sender, eventArgs) => }; ``` -## Adding Another Event Declaration +## Adding another event declaration -Let's add one more feature, and demonstrate other language idioms -for events. Let's add an overload of the `Search` method that -traverses all subdirectories in search of files. +Let's add one more feature, and demonstrate other language idioms for events. Let's add an overload of the `Search` method that traverses all subdirectories in search of files. -This could get to be a lengthy operation in a directory with many -sub-directories. Let's add an event that gets raised when each new -directory search begins. This enables subscribers to track progress, -and update the user as to progress. All the samples you've created so -far are public. Let's make this one an internal event. That means you -can also make the types used for the arguments internal as well. +This could get to be a lengthy operation in a directory with many sub-directories. Let's add an event that gets raised when each new directory search begins. This enables subscribers to track progress, and update the user as to progress. All the samples you've created so far are public. Let's make this one an internal event. That means you can also make the types used for the arguments internal as well. -You'll start by creating the new EventArgs derived class for -reporting the new directory and progress. +You'll start by creating the new EventArgs derived class for reporting the new directory and progress. [!code-csharp[DirEventArgs](../../samples/snippets/csharp/events/Program.cs#SearchDirEventArgs "Define search directory event arguments")] -Again, you can follow the recommendations to make an immutable -reference type for the event arguments. +Again, you can follow the recommendations to make an immutable reference type for the event arguments. -Next, define the event. This time, you'll use a different syntax. In -addition to using the field syntax, you can explicitly create the -property, with add and remove handlers. In this sample, you won't -need extra code in those handlers, but this shows how -you would create them. +Next, define the event. This time, you'll use a different syntax. In addition to using the field syntax, you can explicitly create the property, with add and remove handlers. In this sample, you won't +need extra code in those handlers, but this shows how you would create them. [!code-csharp[Declare event with add and remove handlers](../../samples/snippets/csharp/events/Program.cs#DeclareSearchEvent "Declare the event with add and remove handlers")] -In many ways, the code you write here mirrors the code the compiler -generates for the field event definitions you've seen earlier. You -create the event using syntax very similar to that used for -[properties](properties.md). Notice that the handlers have different -names: `add` and `remove`. These are called to subscribe to the event, -or unsubscribe from the event. Notice that you also must declare a -private backing field to store the event variable. It is initialized -to null. +In many ways, the code you write here mirrors the code the compiler generates for the field event definitions you've seen earlier. You create the event using syntax very similar to that used for [properties](properties.md). Notice that the handlers have different names: `add` and `remove`. These are called to subscribe to the event, or unsubscribe from the event. Notice that you also must declare a private backing field to store the event variable. It is initialized to null. -Next, let's add the overload of the `Search` method that traverses -subdirectories and raises both events. The easiest way to accomplish -this is to use a default argument to specify that you want to search -all directories: +Next, let's add the overload of the `Search` method that traverses subdirectories and raises both events. The easiest way to accomplish this is to use a default argument to specify that you want to search all directories: [!code-csharp[SearchImplementation](../../samples/snippets/csharp/events/Program.cs#FinalImplementation "Implementation to search directories")] -At this point, you can run the application calling the overload for -searching all sub-directories. There are no subscribers on the new -`DirectoryChanged` event, but using the `?.Invoke()` idiom ensures -that this works correctly. +At this point, you can run the application calling the overload for searching all sub-directories. There are no subscribers on the new `DirectoryChanged` event, but using the `?.Invoke()` idiom ensures that this works correctly. - Let's add a handler to write a line that shows the progress in the - console window. +Let's add a handler to write a line that shows the progress in the console window. [!code-csharp[Search](../../samples/snippets/csharp/events/Program.cs#Search "Declare event handler")] -You've seen patterns that are followed throughout the .NET ecosystem. -By learning these patterns and conventions, you'll be writing -idiomatic C# and .NET quickly. +You've seen patterns that are followed throughout the .NET ecosystem. By learning these patterns and conventions, you'll be writing idiomatic C# and .NET quickly. + +## See also + +- [Introduction to events](events-overview.md) +- [Event design](../standard/design-guidelines/event.md) +- [Handle and raise events](../standard/events/index.md) -Next, you'll see some changes in these patterns in the most recent -release of .NET. +Next, you'll see some changes in these patterns in the most recent release of .NET. -[Next](modern-events.md) +> [!div class="step-by-step"] +> [Next](modern-events.md) diff --git a/docs/fundamentals/code-analysis/quality-rules/ca1070.md b/docs/fundamentals/code-analysis/quality-rules/ca1070.md index a9c4d997711d4..5bb8bad3d2161 100644 --- a/docs/fundamentals/code-analysis/quality-rules/ca1070.md +++ b/docs/fundamentals/code-analysis/quality-rules/ca1070.md @@ -20,7 +20,7 @@ ms.author: mavasani ## Cause -A [field-like event](../../../csharp/event-pattern.md#defining-and-raising-field-like-events) was declared as virtual. +A [field-like event](../../../csharp/event-pattern.md#define-and-raise-field-like-events) was declared as virtual. By default, this rule only looks at externally visible types, but this is [configurable](#configure-code-to-analyze). diff --git a/docs/fundamentals/code-analysis/quality-rules/design-warnings.md b/docs/fundamentals/code-analysis/quality-rules/design-warnings.md index 8ced4a29769da..bd27bbe358c08 100644 --- a/docs/fundamentals/code-analysis/quality-rules/design-warnings.md +++ b/docs/fundamentals/code-analysis/quality-rules/design-warnings.md @@ -68,4 +68,4 @@ Design rules support adherence to the [.NET Framework design guidelines](../../. | [CA1067: Override Equals when implementing IEquatable](ca1067.md) | A type implements , but does not override method. | | [CA1068: CancellationToken parameters must come last](ca1068.md) | A method has a CancellationToken parameter that is not the last parameter. | | [CA1069: Enums should not have duplicate values](ca1069.md) | An enumeration has multiple members which are explicitly assigned the same constant value. | -| [CA1070: Do not declare event fields as virtual](ca1070.md) | A [field-like event](../../../csharp/event-pattern.md#defining-and-raising-field-like-events) was declared as virtual. | +| [CA1070: Do not declare event fields as virtual](ca1070.md) | A [field-like event](../../../csharp/event-pattern.md#define-and-raise-field-like-events) was declared as virtual. | diff --git a/docs/fundamentals/code-analysis/quality-rules/index.md b/docs/fundamentals/code-analysis/quality-rules/index.md index 66611f3b3e521..f6ee5831234b5 100644 --- a/docs/fundamentals/code-analysis/quality-rules/index.md +++ b/docs/fundamentals/code-analysis/quality-rules/index.md @@ -65,7 +65,7 @@ The following table lists code quality analysis rules. > | [CA1067: Override Equals when implementing IEquatable](ca1067.md) | A type implements , but does not override method. | > | [CA1068: CancellationToken parameters must come last](ca1068.md) | A method has a CancellationToken parameter that is not the last parameter. | > | [CA1069: Enums should not have duplicate values](ca1069.md) | An enumeration has multiple members which are explicitly assigned the same constant value. | -> | [CA1070: Do not declare event fields as virtual](ca1070.md) | A [field-like event](../../../csharp/event-pattern.md#defining-and-raising-field-like-events) was declared as virtual. | +> | [CA1070: Do not declare event fields as virtual](ca1070.md) | A [field-like event](../../../csharp/event-pattern.md#define-and-raise-field-like-events) was declared as virtual. | > | [CA1200: Avoid using cref tags with a prefix](ca1200.md) | The [cref](../../../csharp/language-reference/xmldoc/recommended-tags.md) attribute in an XML documentation tag means "code reference". It specifies that the inner text of the tag is a code element, such as a type, method, or property. Avoid using `cref` tags with prefixes, because it prevents the compiler from verifying references. It also prevents the Visual Studio integrated development environment (IDE) from finding and updating these symbol references during refactorings. | > | [CA1303: Do not pass literals as localized parameters](ca1303.md) | An externally visible method passes a string literal as a parameter to a .NET constructor or method, and that string should be localizable. | > | [CA1304: Specify CultureInfo](ca1304.md) | A method or constructor calls a member that has an overload that accepts a System.Globalization.CultureInfo parameter, and the method or constructor does not call the overload that takes the CultureInfo parameter. When a CultureInfo or System.IFormatProvider object is not supplied, the default value that is supplied by the overloaded member might not have the effect that you want in all locales. | diff --git a/samples/snippets/csharp/events/NuGet.Config b/samples/snippets/csharp/events/NuGet.Config deleted file mode 100644 index 2f06743762391..0000000000000 --- a/samples/snippets/csharp/events/NuGet.Config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/samples/snippets/csharp/events/Program.cs b/samples/snippets/csharp/events/Program.cs index a5565f3a32a20..feb97f32b08e8 100644 --- a/samples/snippets/csharp/events/Program.cs +++ b/samples/snippets/csharp/events/Program.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; +using System.IO; namespace EventSampleCode { @@ -15,7 +9,7 @@ static void Main(string[] args) // var fileLister = new FileSearcher(); int filesFound = 0; - + EventHandler onFileFound = (sender, eventArgs) => { Console.WriteLine(eventArgs.FoundFile); @@ -45,12 +39,9 @@ static void Main(string[] args) public class FileFoundArgs : EventArgs { public string FoundFile { get; } - public bool CancelRequested { get; set;} + public bool CancelRequested { get; set; } - public FileFoundArgs(string fileName) - { - FoundFile = fileName; - } + public FileFoundArgs(string fileName) => FoundFile = fileName; } // @@ -78,10 +69,10 @@ public class FileSearcher // internal event EventHandler DirectoryChanged { - add { directoryChanged += value; } - remove { directoryChanged -= value; } + add { _directoryChanged += value; } + remove { _directoryChanged -= value; } } - private EventHandler directoryChanged; + private EventHandler _directoryChanged; // // @@ -94,14 +85,13 @@ public void Search(string directory, string searchPattern, bool searchSubDirs = var totalDirs = allDirectories.Length + 1; foreach (var dir in allDirectories) { - directoryChanged?.Invoke(this, - new SearchDirectoryArgs(dir, totalDirs, completedDirs++)); + RaiseSearchDirectoryChanged(dir, totalDirs, completedDirs++); // Search 'dir' and its subdirectories for files that match the search pattern: SearchDirectory(dir, searchPattern); } // Include the Current Directory: - directoryChanged?.Invoke(this, - new SearchDirectoryArgs(directory, totalDirs, completedDirs++)); + RaiseSearchDirectoryChanged(directory, totalDirs, completedDirs++); + SearchDirectory(directory, searchPattern); } else @@ -109,17 +99,31 @@ public void Search(string directory, string searchPattern, bool searchSubDirs = SearchDirectory(directory, searchPattern); } } - + private void SearchDirectory(string directory, string searchPattern) { foreach (var file in Directory.EnumerateFiles(directory, searchPattern)) { - var args = new FileFoundArgs(file); - FileFound?.Invoke(this, args); + FileFoundArgs args = RaiseFileFound(file); if (args.CancelRequested) + { break; + } } } + + private void RaiseSearchDirectoryChanged( + string directory, int totalDirs, int completedDirs) => + _directoryChanged?.Invoke( + this, + new SearchDirectoryArgs(directory, totalDirs, completedDirs)); + + private FileFoundArgs RaiseFileFound(string file) + { + var args = new FileFoundArgs(file); + FileFound?.Invoke(this, args); + return args; + } // } } @@ -131,10 +135,7 @@ public class FileFoundArgs : EventArgs { public string FoundFile { get; } - public FileFoundArgs(string fileName) - { - FoundFile = fileName; - } + public FileFoundArgs(string fileName) => FoundFile = fileName; } // @@ -147,9 +148,12 @@ public void Search(string directory, string searchPattern) { foreach (var file in Directory.EnumerateFiles(directory, searchPattern)) { - FileFound?.Invoke(this, new FileFoundArgs(file)); + RaiseFileFound(file); } } + + private void RaiseFileFound(string file) => + FileFound?.Invoke(this, new FileFoundArgs(file)); } // } diff --git a/samples/snippets/csharp/events/events.csproj b/samples/snippets/csharp/events/events.csproj index c73e0d1692ab3..fb76edd0d461a 100644 --- a/samples/snippets/csharp/events/events.csproj +++ b/samples/snippets/csharp/events/events.csproj @@ -2,7 +2,8 @@ Exe - netcoreapp3.1 + net6.0 + enable