diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..e539b50340 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,137 @@ +# Overview +This document provides guidelines for using GitHub Copilot to contribute to the .NET MAUI Community Toolkit. It includes instructions on setting up your environment, writing code, and following best practices specific to .NET MAUI. + +## Prerequisites +1. Install the latest stable (.NET SDK)[https://dotnet.microsoft.com/en-us/download]. +2. Install .NET MAUI workloads (we recommend using Visual Studio installer). + +## Setting Up GitHub Copilot +1. Ensure you have GitHub Copilot installed and enabled in Visual Studio. +2. Familiarize yourself with the basic usage of GitHub Copilot by reviewing the (official documentation)[https://docs.github.com/en/copilot]. + +## Writing Code with GitHub Copilot +### General Guidelines +* Use GitHub Copilot to assist with code completion, documentation, and generating boilerplate code. +* Always review and test the code suggested by GitHub Copilot to ensure it meets the project's standards and requirements. + +### Specific to .NET MAUI +* Ensure that any UI components or controls are compatible with .NET MAUI. +* Avoid using Xamarin.Forms-specific code unless there is a direct .NET MAUI equivalent. +* Follow the project's coding style and best practices as outlined in the (contributing)[https://github.com/CommunityToolkit/Maui/blob/main/CONTRIBUTING.md] document. + +## Best Practices +* Use **Trace.WriteLine()** for debug logging instead of **Debug.WriteLine()**. +* Include a **CancellationToken** as a parameter for methods returning **Task** or **ValueTask**. +* Use **is** for null checking and type checking. +* Use file-scoped namespaces to reduce code verbosity. +* Avoid using the **!** null forgiving operator. +** Follow naming conventions for enums and property names. + +### Debug Logging +* Always use `Trace.WriteLine()` instead of `Debug.WriteLine` for debug logging because `Debug.WriteLine` is removed by the compiler in Release builds + +### Methods Returning Task and ValueTask +* Always include a `CancellationToken` as a parameter to every method returning `Task` or `ValueTask` +* If the method is public, provide a the default value for the `CancellationToken` (e.g. `CancellationToken token = default`) +* If the method is not public, do not provide a default value for the `CancellationToken` +* If the method is used outside of a .net MAUI control, Use `CancellationToken.ThrowIfCancellationRequested()` to verify the `CancellationToken`, as it is not possible to catch exceptions in XAML. + +### Enums +* Always use `Unknown` at index 0 for return types that may have a value that is not known +* Always use `Default` at index 0 for option types that can use the system default option +* Follow naming guidelines for tense... `SensorSpeed` not `SensorSpeeds` +* Assign values (0,1,2,3) for all enums, if not marked with a `Flags` attribute. This is to ensure that the enum can be serialized and deserialized correctly across platforms. + +### Property Names +* Include units only if one of the platforms includes it in their implementation. For instance HeadingMagneticNorth implies degrees on all platforms, but PressureInHectopascals is needed since platforms don't provide a consistent API for this. + +### Units +* Use the standard units and most well accepted units when possible. For instance Hectopascals are used on UWP/Android and iOS uses Kilopascals so we have chosen Hectopascals. + +### Pattern matching +#### Null checking +* Prefer using `is` when checking for null instead of `==`. + +e.g. + +```csharp +// null +if (something is null) +{ + +} + +// or not null +if (something is not null) +{ + +} +``` + +* Avoid using the `!` [null forgiving operator](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving) to avoid the unintended introduction of bugs. + +#### Type checking +* Prefer `is` when checking for types instead of casting. + +e.g. + +```csharp +if (something is Bucket bucket) +{ + bucket.Empty(); +} +``` + +### File Scoped Namespaces +* Use [file scoped namespaces](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces) to help reduce code verbosity. + +e.g. + +```csharp +namespace CommunityToolkit.Maui.Converters; + +using System; + +class BoolToObjectConverter +{ +} +``` + +### Braces +Please use `{ }` after `if`, `for`, `foreach`, `do`, `while`, etc. + +e.g. + +```csharp +if (something is not null) +{ + ActOnIt(); +} +``` + +### `NotImplementedException` +* Please avoid adding new code that throws a `NotImplementedException`. According to the [Microsoft Docs](https://docs.microsoft.com/dotnet/api/system.notimplementedexception), we should only "throw a `NotImplementedException` exception in properties or methods in your own types when that member is still in development and will only later be implemented in production code. In other words, a NotImplementedException exception should be synonymous with 'still in development.'" +In other words, `NotImplementedException` implies that a feature is still in development, indicating that the Pull Request is incomplete. + +### Bug Fixes +If you're looking for something to fix, please browse [open issues](https://github.com/CommunityToolkit/Maui/issues). + +Follow the style used by the [.NET Foundation](https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/coding-style.md), with two primary exceptions: + +* We do **not** use the `private` keyword as it is the default accessibility level in C#. +* We will **not** use `_` or `s_` as a prefix for internal or private field names +* We will use `camelCaseFieldName` for naming internal or private fields in both instance and static implementations + +Read and follow our [Pull Request template](https://github.com/CommunityToolkit/Maui/blob/main/.github/PULL_REQUEST_TEMPLATE.md) + +## Submitting Contributions +1. Fork the repository and create a new branch for your changes. +2. Implement your changes using GitHub Copilot as needed. +3. Ensure your changes include tests, samples, and documentation. +4. Open a pull request and follow the [Pull Request template](https://github.com/CommunityToolkit/Maui/blob/main/.github/PULL_REQUEST_TEMPLATE.md). + +## Additional Resources +- (GitHub Copilot Documentation)[https://docs.github.com/en/copilot] +- (.NET MAUI Documentation)[https://learn.microsoft.com/en-us/dotnet/maui/] + +By following these guidelines, you can effectively use GitHub Copilot to contribute to the .NET MAUI Community Toolkit. Thank you for your contributions! \ No newline at end of file diff --git a/.github/prompts/dotnet/async.prompt.md b/.github/prompts/dotnet/async.prompt.md new file mode 100644 index 0000000000..6a560f0be7 --- /dev/null +++ b/.github/prompts/dotnet/async.prompt.md @@ -0,0 +1,32 @@ +# Async Programming Best Practices + +This cheat sheet should serve as a quick reminder of best practices when writing asynchronous code. Following these guidelines will help you avoid common pitfalls such as unobserved exceptions, deadlocks, and unexpected UI blocking. + +## DOs +- **Always Await Your Tasks:** + Always use the `await` keyword on async operations to ensure exceptions are captured and to avoid blocking the calling thread. +- **Use Async Task/Task Methods:** + Prefer returning `Task` or `Task` over `async void` so that exceptions can be observed, and methods are easily composable and testable. +- **Name Methods with the "Async" Suffix:** + Clearly differentiate asynchronous methods (e.g., `GetDataAsync()`) from synchronous ones, if there's a synchronous counterpart. +- **Pass Cancellation Tokens:** + Allow cancellation by accepting a `CancellationToken` in async methods. +- **Use ConfigureAwait(false) When Appropriate:** + In library code use `ConfigureAwait(false)` to avoid unnecessary context switches and potential deadlocks. +- **Keep Async Code “Async All the Way”:** + Propagate async all the way from the entry point (like event handlers or controller actions) rather than mixing sync and async code. +- **Report Progress and Handle Exceptions Properly:** + Use tools like `IProgress` to report progress and always catch exceptions at the appropriate level when awaiting tasks. + +## DON'Ts +- **Avoid async void Methods:** + Except for event handlers and overriding methods, never use `async void` because their exceptions are not observable and they're difficult to test. +- **Don't Block on Async Code:** + Avoid using `.Wait()` or `.Result` as these can lead to deadlocks and wrap exceptions in `AggregateException`. If you must block, consider using `GetAwaiter().GetResult()`. +- **Don't Mix Blocking and Async Code:** + Blocking the calling thread in an otherwise async flow (e.g., by mixing synchronous calls with async ones) may cause deadlocks and performance issues. +- **Avoid Wrapping Return Task in Try/Catch or Using Blocks:** + When a method returns a `Task`, wrapping it in a try/catch or using block may lead to unexpected behaviour because the task completes outside those blocks. For these scenarios use `async/await`. +- **Don't Overuse Fire-and-Forget Patterns:** + Unobserved tasks (fire-and-forget) can swallow exceptions and cause race conditions—if needed, use a “safe fire-and-forget” pattern with proper error handling. + \ No newline at end of file diff --git a/.github/prompts/dotnet/codestyle.prompt.md b/.github/prompts/dotnet/codestyle.prompt.md new file mode 100644 index 0000000000..68c8143a81 --- /dev/null +++ b/.github/prompts/dotnet/codestyle.prompt.md @@ -0,0 +1,612 @@ +# Coding Style Guide +This guide provides a set of best practices and coding standards for writing C# code with GitHub Copilot. It covers various aspects of C# programming, including naming conventions, code structure, control flow, nullability, safe operations, asynchronous programming, and symbol references. + +## Type Definitions: +- Prefer records for data types: + ```csharp + // Good: Immutable data type with value semantics + public sealed record CustomerDto(string Name, Email Email); + + // Avoid: Class with mutable properties + public class Customer + { + public string Name { get; set; } + public string Email { get; set; } + } + ``` +- Make classes sealed by default: + ```csharp + // Good: Sealed by default + public sealed class OrderProcessor + { + // Implementation + } + + // Only unsealed when inheritance is specifically designed for + public abstract class Repository + { + // Base implementation + } + ``` + +## Control Flow + +- Prefer range indexers over LINQ: + ```csharp + // Good: Using range indexers with clear comments + var lastItem = items[^1]; // ^1 means "1 from the end" + var firstThree = items[..3]; // ..3 means "take first 3 items" + var slice = items[2..5]; // take items from index 2 to 4 (5 exclusive) + + // Avoid: Using LINQ when range indexers are clearer + var lastItem = items.LastOrDefault(); + var firstThree = items.Take(3).ToList(); + var slice = items.Skip(2).Take(3).ToList(); + ``` +- Prefer collection initializers: + ```csharp + // Good: Using collection initializers + string[] fruits = ["Maui", "Community", "Toolkit"]; + + // Avoid: Using explicit initialization when type is clear + var fruits = new List() { + "Maui", + "Community", + "Toolkit" + }; + ``` +- Use pattern matching effectively: + ```csharp + // Good: Clear pattern matching + static bool TryGetModalPageButton(Microsoft.Maui.ILayout layout, [NotNullWhen(true)] out Button? button) + { + button = null; + + foreach (var view in layout) + { + switch (view) + { + case Button { Text: tryStatusBarOnModalPageButtonText } modalPageButton: + button = modalPageButton; + return true; + + case Microsoft.Maui.ILayout nestedLayout: + if (TryGetModalPageButton(nestedLayout, out var modalPageButtonInLayout)) + { + button = modalPageButtonInLayout; + return true; + } + break; + } + } + + return false; + } + + // Avoid: Nested if statements + static bool TryGetModalPageButton(Microsoft.Maui.ILayout layout, [NotNullWhen(true)] out Button? button) + { + button = null; + + foreach (var view in layout) + { + if (view is Button { Text: tryStatusBarOnModalPageButtonText } modalPageButton) + { + button = modalPageButton; + return true; + } + else if (view is Microsoft.Maui.ILayout nestedLayout) + { + if (TryGetModalPageButton(nestedLayout, out var modalPageButtonInLayout)) + { + button = modalPageButtonInLayout; + return true; + } + } + } + + return false; + } + ``` + +## Nullability: + +- Mark nullable fields explicitly: + ```csharp + // Good: Explicit nullability + public partial class MaskedBehavior : BaseBehavior, IDisposable + { + IReadOnlyDictionary? maskPositions; + + void SetMaskPositions(in string? mask) + { + if (string.IsNullOrEmpty(mask)) + { + maskPositions = null; + return; + } + + var list = new Dictionary(); + + for (var i = 0; i < mask.Length; i++) + { + if (mask[i] != UnmaskedCharacter) + { + list.Add(i, mask[i]); + } + } + + maskPositions = list; + } + } + + // Avoid: Implicit nullability + public partial class MaskedBehavior : BaseBehavior, IDisposable + { + IReadOnlyDictionary maskPositions; // Warning: Could be null + private string lastPosition; // Warning: Could be null + } + ``` +- Use null checks only when necessary for reference types and public methods: + ```csharp + // Good: Proper null checking + void HandleButtonClicked(object? sender, EventArgs e) + { + ArgumentNullException.ThrowIfNull(sender); + var button = (Button)sender; + button.Behaviors.Add(new IconTintColorBehavior + { + TintColor = Colors.Green + }); + } + + // Good: Using pattern matching for null checks + void OnRegexPropertyChanged(string? regexPattern, RegexOptions regexOptions) => regex = regexPattern switch + { + null => null, + _ => new Regex(regexPattern, regexOptions) + }; + + // Avoid: null checks for value types + public void ProcessOrder(int orderId) + { + ArgumentNullException.ThrowIfNull(orderId); + } + + // Avoid: null checks for non-public methods + private void ProcessOrder(Order order) + { + ArgumentNullException.ThrowIfNull(order); + } + ``` +- Use null-forgiving operator only when appropriate: + ```csharp + public class OrderValidator + { + private readonly IValidator _validator; + + public OrderValidator(IValidator validator) + { + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + } + + public ValidationResult Validate(Order order) + { + // We know _validator can't be null due to constructor check + return _validator!.Validate(order); + } + } + ``` +- Use nullability attributes: + ```csharp + [AcceptEmptyServiceProvider] + public partial class ImageResourceConverter : BaseConverterOneWay + { + [return: NotNullIfNotNull(nameof(value))] + public override ImageSource? ConvertFrom(string? value, CultureInfo? culture = null) => value switch + { + null => null, + _ => ImageSource.FromResource(value, Application.Current?.GetType().Assembly) + }; + + // Method never returns null + [return: NotNull] + public static string EnsureNotNull(string? input) => + input ?? string.Empty; + + // Parameter must not be null when method returns true + public static bool TryParse(string? input, [NotNullWhen(true)] out string? result) + { + result = null; + if (string.IsNullOrEmpty(input)) + return false; + + result = input; + return true; + } + } + ``` +- Use init-only properties with non-null validation: + ```csharp + // Good: Non-null validation in constructor + public class AvatarModel + { + public Color BackgroundColor { get; init; } = Colors.Black; + public Color BorderColor { get; init; } = Colors.White; + + public AvatarModel() + { + BackgroundColor = null!; // Will be set by required property + BorderColor = null!; // Will be set by required property + } + + private AvatarModel(Color backgroundColor, Color borderColor) + { + BackgroundColor = backgroundColor; + BorderColor = borderColor; + } + + public static AvatarModel Create(Color backgroundColor, Color borderColor) => + new(backgroundColor, borderColor); + } + ``` +- Document nullability in interfaces: + ```csharp + public interface IOrderRepository + { + // Explicitly shows that null is a valid return value + Task FindByIdAsync(OrderId id, CancellationToken ct = default); + + // Method will never return null + [return: NotNull] + Task> GetAllAsync(CancellationToken ct = default); + + // Parameter cannot be null + Task SaveAsync([NotNull] Order order, CancellationToken ct = default); + } + ``` + +## Safe Operations: + +- Use Try methods for safer operations: + ```csharp + // Good: Using TryGetValue for dictionary access + if (dictionary.TryGetValue(key, out var value)) + { + // Use value safely here + } + else + { + // Handle missing key case + } + ``` + ```csharp + // Avoid: Direct indexing which can throw + var value = dictionary[key]; // Throws if key doesn't exist + + // Good: Using Uri.TryCreate for URL parsing + if (Uri.TryCreate(urlString, UriKind.Absolute, out var uri)) + { + // Use uri safely here + } + else + { + // Handle invalid URL case + } + ``` + ```csharp + // Avoid: Direct Uri creation which can throw + var uri = new Uri(urlString); // Throws on invalid URL + + // Good: Using int.TryParse for number parsing + if (int.TryParse(input, out var number)) + { + // Use number safely here + } + else + { + // Handle invalid number case + } + ``` + ```csharp + // Good: Combining Try methods with null coalescing + var value = dictionary.TryGetValue(key, out var result) + ? result + : defaultValue; + + // Good: Using Try methods in LINQ with pattern matching + var validNumbers = strings + .Select(s => (Success: int.TryParse(s, out var num), Value: num)) + .Where(x => x.Success) + .Select(x => x.Value); + ``` + +- Prefer Try methods over exception handling: + ```csharp + // Good: Using Try method + if (decimal.TryParse(priceString, out var price)) + { + // Process price + } + + // Avoid: Exception handling for expected cases + try + { + var price = decimal.Parse(priceString); + // Process price + } + catch (FormatException) + { + // Handle invalid format + } + ``` + +## Asynchronous Programming: + +- Use Task.FromResult for pre-computed values: + ```csharp + // Good: Return pre-computed value + public Task GetDefaultQuantityAsync() => + Task.FromResult(1); + + // Better: Use ValueTask for zero allocations + public ValueTask GetDefaultQuantityAsync() => + new ValueTask(1); + + // Avoid: Unnecessary thread pool usage + public Task GetDefaultQuantityAsync() => + Task.Run(() => 1); + ``` +- Always flow CancellationToken: + ```csharp + // Good: Propagate cancellation + public async Task ProcessOrderAsync( + OrderRequest request, + CancellationToken cancellationToken) + { + var order = await _repository.GetAsync( + request.OrderId, + cancellationToken); + + await _processor.ProcessAsync( + order, + cancellationToken); + + return order; + } + ``` +- Prefer await: + ```csharp + // Good: Using await + public async Task ProcessOrderAsync(OrderId id) + { + var order = await _repository.GetAsync(id); + await _validator.ValidateAsync(order); + return order; + } + ``` +- Never use Task.Result or Task.Wait: + ```csharp + // Good: Async all the way + public async Task GetOrderAsync(OrderId id) + { + return await _repository.GetAsync(id); + } + + // Avoid: Blocking on async code + public Order GetOrder(OrderId id) + { + return _repository.GetAsync(id).Result; // Can deadlock + } + ``` +- Use TaskCompletionSource correctly: + ```csharp + // Good: Using RunContinuationsAsynchronously + private readonly TaskCompletionSource _tcs = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + // Avoid: Default TaskCompletionSource can cause deadlocks + private readonly TaskCompletionSource _tcs = new(); + ``` +- Always dispose CancellationTokenSources: + ```csharp + // Good: Proper disposal of CancellationTokenSource + public async Task GetOrderWithTimeout(OrderId id) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + return await _repository.GetAsync(id, cts.Token); + } + ``` +- Prefer async/await over direct Task return: + ```csharp + // Good: Using async/await + public async Task ProcessOrderAsync(OrderRequest request) + { + await _validator.ValidateAsync(request); + var order = await _factory.CreateAsync(request); + return order; + } + + // Avoid: Manual task composition + public Task ProcessOrderAsync(OrderRequest request) + { + return _validator.ValidateAsync(request) + .ContinueWith(t => _factory.CreateAsync(request)) + .Unwrap(); + } + ``` + +## Symbol References: + +- Always use nameof operator: + ```csharp + // Good: Using nameof in attributes + public class OrderProcessor + { + [Required(ErrorMessage = "The {0} field is required")] + [Display(Name = nameof(OrderId))] + public string OrderId { get; init; } + + [MemberNotNull(nameof(_repository))] + private void InitializeRepository() + { + _repository = new OrderRepository(); + } + + [NotifyPropertyChangedFor(nameof(FullName))] + public string FirstName + { + get => _firstName; + set => SetProperty(ref _firstName, value); + } + } + ``` +- Use nameof with exceptions: + ```csharp + public class OrderService + { + public async Task GetOrderAsync(OrderId id, CancellationToken ct) + { + var order = await _repository.FindAsync(id, ct); + + if (order is null) + throw new OrderNotFoundException( + $"Order with {nameof(id)} '{id}' not found"); + + if (!order.Lines.Any()) + throw new InvalidOperationException( + $"{nameof(order.Lines)} cannot be empty"); + + return order; + } + + public void ValidateOrder(Order order) + { + if (order.Lines.Count == 0) + throw new ArgumentException( + "Order must have at least one line", + nameof(order)); + } + } + ``` +- Use nameof in logging: + ```csharp + public class OrderProcessor + { + private readonly ILogger _logger; + + public async Task ProcessAsync(Order order) + { + _logger.LogInformation( + "Starting {Method} for order {OrderId}", + nameof(ProcessAsync), + order.Id); + + try + { + await ProcessInternalAsync(order); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error in {Method} for {Property} {Value}", + nameof(ProcessAsync), + nameof(order.Id), + order.Id); + throw; + } + } + } + ``` + +### Usings and Namespaces: + +- Use implicit usings: + ```csharp + // Good: Implicit + namespace MyNamespace + { + public class MyClass + { + // Implementation + } + } + // Avoid: + using System; // DON'T USE + using System.Collections.Generic; // DON'T USE + using System.IO; // DON'T USE + using System.Linq; // DON'T USE + using System.Net.Http; // DON'T USE + using System.Threading; // DON'T USE + using System.Threading.Tasks;// DON'T USE + using System.Net.Http.Json; // DON'T USE + using Microsoft.AspNetCore.Builder; // DON'T USE + using Microsoft.AspNetCore.Hosting; // DON'T USE + using Microsoft.AspNetCore.Http; // DON'T USE + using Microsoft.AspNetCore.Routing; // DON'T USE + using Microsoft.Extensions.Configuration; // DON'T USE + using Microsoft.Extensions.DependencyInjection; // DON'T USE + using Microsoft.Extensions.Hosting; // DON'T USE + using Microsoft.Extensions.Logging; // DON'T USE + using Good: Explicit usings; // DON'T USE + + namespace MyNamespace + { + public class MyClass + { + // Implementation + } + } + ``` +- Use file-scoped namespaces: + ```csharp + // Good: File-scoped namespace + namespace MyNamespace; + + public class MyClass + { + // Implementation + } + + // Avoid: Block-scoped namespace + namespace MyNamespace + { + public class MyClass + { + // Implementation + } + } + ``` + +### Element Positioning + +Please adhere to [Style Cop SA1201](https://docs.github.com/en/copilot/using-github-copilot/code-review/configuring-coding-guidelines#creating-a-coding-guideline) for organizing code in a file. + +Elements at the file root level or within a namespace should be positioned in the following order: + +Extern Alias Directives +Using Directives +Namespaces +Delegates +Enums +Interfaces +Records +Structs +Classes + +Within a class, struct, or interface, elements should be positioned in the following order: + +Fields +Constructors +Finalizers (Destructors) +Delegates +Events +Enums +Interfaces +Properties +Indexers +Methods +Records +Structs +Classes \ No newline at end of file diff --git a/.github/prompts/dotnet/maui/maui-controls.prompt.md b/.github/prompts/dotnet/maui/maui-controls.prompt.md new file mode 100644 index 0000000000..b9d0c8c9f1 --- /dev/null +++ b/.github/prompts/dotnet/maui/maui-controls.prompt.md @@ -0,0 +1,349 @@ +# Page Life-cycle + +Use `EventToCommandBehavior` from CommunityToolkit.Maui to handle page life-cycle events when using XAML. + +```xml + + + + + + + + + +``` + +## Control Choices + +* Prefer `Grid` over other layouts to keep the visual tree flatter +* Use `VerticalStackLayout` or `HorizontalStackLayout`, not `StackLayout` +* Use `CollectionView` or a `BindableLayout`, not `ListView` or `TableView` +* Use `Border`, not `Frame` +* Declare `ColumnDefinitions` and `RowDefinitions` in-line like `` + +## Common Code Styles and Implementation Templates Specific to .NET MAUI + +### Using `ICommand` for Commanding +- **Good Practice**: Use `ICommand` for handling user interactions in MVVM pattern. +- **Bad Practice**: Handling user interactions directly in the code-behind. + +```csharp +// Good Practice +public class MainPageViewModel +{ + public ICommand ButtonCommand { get; } + + public MainPageViewModel() + { + ButtonCommand = new Command(OnButtonClicked); + } + + private void OnButtonClicked() + { + // Handle button click + } +} + +// Bad Practice +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + Button.Clicked += OnButtonClicked; + } + + private void OnButtonClicked(object sender, EventArgs e) + { + // Handle button click + } +} +``` + +### Using `ObservableCollection` for Data Binding +- **Good Practice**: Use `ObservableCollection` for collections that need to notify the UI of changes. +- **Bad Practice**: Using `List` for collections that need to notify the UI of changes. + +```csharp +// Good Practice +public class MainPageViewModel +{ + public ObservableCollection Items { get; } = new ObservableCollection(); + + public MainPageViewModel() + { + Items.Add("Item 1"); + Items.Add("Item 2"); + } +} + +// Bad Practice +public class MainPageViewModel +{ + public List Items { get; } = new List(); + + public MainPageViewModel() + { + Items.Add("Item 1"); + Items.Add("Item 2"); + } +} +``` + +### Using `DataTemplate` for Customizing List Items +- **Good Practice**: Use `DataTemplate` to define the appearance of items in a collection view. +- **Bad Practice**: Defining item appearance directly in the collection view. + +```csharp +// Good Practice + + + + + + + + + +// Bad Practice + + + + +``` + +### Using `OnAppearing` and `OnDisappearing` for Page Lifecycle Events +- **Good Practice**: Override `OnAppearing` and `OnDisappearing` to handle page lifecycle events. +- **Bad Practice**: Using event handlers for page lifecycle events. + +```csharp +// Good Practice +public class MainPage : ContentPage +{ + protected override void OnAppearing() + { + base.OnAppearing(); + // Handle appearing + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + // Handle disappearing + } +} + +// Bad Practice +public class MainPage : ContentPage +{ + public MainPage() + { + Appearing += OnPageAppearing; + Disappearing += OnPageDisappearing; + } + + private void OnPageAppearing(object sender, EventArgs e) + { + // Handle appearing + } + + private void OnPageDisappearing(object sender, EventArgs e) + { + // Handle disappearing + } +} +``` + +### Using Resource Dictionaries +- **Good Practice**: Use resource dictionaries to manage styles and resources. +- **Bad Practice**: Defining styles and resources directly in XAML files without using resource dictionaries. + +```csharp +// Good Practice + + + + + + +// Bad Practice + + + + + +``` + +### Using `ResourceDictionary` for Theming +- **Good Practice**: Use `ResourceDictionary` to manage themes and styles. +- **Bad Practice**: Defining themes and styles directly in XAML files without using `ResourceDictionary`. + +```csharp +// Good Practice + + + + + + + + + +// Bad Practice + + + + + +``` + +### Using `Shell` for Navigation +- **Good Practice**: Use `Shell` for navigation to simplify and standardize navigation patterns. +- **Bad Practice**: Using `NavigationPage` and `TabbedPage` directly for complex navigation scenarios. + +```csharp +// Good Practice +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage)); + } +} + +// Bad Practice +public class App : Application +{ + public App() + { + MainPage = new NavigationPage(new MainPage()); + } +} +``` + +### Using Data Binding +- **Good Practice**: Use data binding to bind UI elements to view models. +- **Bad Practice**: Directly manipulating UI elements in code-behind. + +```csharp +// Good Practice +public class MainPageViewModel : INotifyPropertyChanged +{ + private string _text; + public string Text + { + get => _text; + set + { + _text = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} + +// Bad Practice +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + var entry = new Entry(); + entry.TextChanged += (s, e) => { entry.Text = "New Text"; }; + } +} +``` + +## LINQ Usage +- **Good Practice**: Use LINQ for concise and readable data manipulation. +- **Bad Practice**: Using loops for operations that can be done with LINQ. + +```csharp +// Good Practice +var activeUsers = users.Where(u => u.IsActive).ToList(); + +// Bad Practice +var activeUsers = new List(); +foreach (var user in users) +{ + if (user.IsActive) + { + activeUsers.Add(user); + } +} +``` + +## Using DependencyService +- **Good Practice**: Use `DependencyService` to resolve platform-specific implementations. +- **Bad Practice**: Directly referencing platform-specific code in shared code. + +**Example:** +```csharp +// Good Practice +public interface IPlatformService +{ + void PerformPlatformSpecificOperation(); +} + +public class MainPage : ContentPage +{ + public MainPage() + { + var platformService = DependencyService.Get(); + platformService?.PerformPlatformSpecificOperation(); + } +} + +// Bad Practice +public class MainPage : ContentPage +{ +#if ANDROID + public MainPage() + { + // Android specific code + } +#elif IOS + public MainPage() + { + // iOS specific code + } +#endif +} +``` diff --git a/.github/prompts/dotnet/maui/maui-memory-leaks.prompt.md b/.github/prompts/dotnet/maui/maui-memory-leaks.prompt.md new file mode 100644 index 0000000000..a45ac1dc95 --- /dev/null +++ b/.github/prompts/dotnet/maui/maui-memory-leaks.prompt.md @@ -0,0 +1,140 @@ +## Memory Leaks + +### Avoid circular references on iOS and Catalyst + +C# objects co-exist with a reference-counted world on Apple platforms, and so a C# object that subclasses `NSObject` can run into situations where they can accidentally live forever -- a memory leak. This situation does not occur on Android or Windows platforms. + +Here is an example of a circular reference: + +```csharp +class MyViewSubclass : UIView +{ + public UIView? Parent { get; set; } + + public void Add(MyViewSubclass subview) + { + subview.Parent = this; + AddSubview(subview); + } +} + +//... + +var parent = new MyViewSubclass(); +var view = new MyViewSubclass(); +parent.Add(view); +``` + +In this case: + +* `parent` -> `view` via `Subviews` +* `view` -> `parent` via the `Parent` property +* The reference count of both objects is non-zero +* Both objects live forever + +This problem isn't limited to a field or property. A similar situation may occur with C# events: + +```csharp +class MyView : UIView +{ + public MyView() + { + var picker = new UIDatePicker(); + AddSubview(picker); + picker.ValueChanged += OnValueChanged; + } + + void OnValueChanged(object? sender, EventArgs e) { } + + // Use this instead and it doesn't leak! + //static void OnValueChanged(object? sender, EventArgs e) { } +} +``` + +In this case: + +* `MyView` -> `UIDatePicker` via `Subviews` +* `UIDatePicker` -> `MyView` via `ValueChanged` and `EventHandler.Target` +* Both objects live forever + +A solution for this example, is to make `OnValueChanged` method `static`, which would result in a `null` `Target` on the `EventHandler` instance. + +Another solution, would be to put `OnValueChanged` in a non-`NSObject` subclass: + +```csharp +class MyView : UIView +{ + readonly Proxy _proxy = new(); + + public MyView() + { + var picker = new UIDatePicker(); + AddSubview(picker); + picker.ValueChanged += _proxy.OnValueChanged; + } + + class Proxy + { + public void OnValueChanged(object? sender, EventArgs e) { } + } +} +``` + +If the class subscribing to the events are not an `NSObject` subclass, we can also use a proxy (use weak references to the primary object). + +An example is a "view handler" which maps a "virtual view" to a "platform view". + +The handler will have a readonly field for the proxy, and the proxy will manage the events between the platform view and the virtual view. The proxy should not have a reference to the handler as the handler does not take part in the events. + +* The handler has a strong reference to the proxy +* The platform view has a strong reference to the proxy via the event handler +* The proxy has a _weak_ reference to the virtual view + +```csharp +class DatePickerHandler : ViewHandler +{ + readonly Proxy proxy = new(); + + protected override void ConnectHandler(UIDatePicker platformView) + { + proxy.Connect(VirtualView, platformView); + base.ConnectHandler(platformView); + } + + protected override void DisconnectHandler(UIDatePicker platformView) + { + proxy.Disconnect(VirtualView, picker); + + base.DisconnectHandler(platformView); + } + + void OnValueChanged() { } + + class Proxy + { + WeakReference? _virtualView; + + IDatePicker? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null; + + public void Connect(IDatePicker handler, UIDatePicker platformView) + { + _virtualView = new(handler); + platformView.ValueChanged += OnValueChanged; + } + + public void Disconnect(UIDatePicker platformView) + { + _virtualView = null; + + platformView.ValueChanged -= OnValueChanged; + } + + public void OnValueChanged(object? sender, EventArgs e) + { + VirtualView?.OnValueChanged(); + } + } +} +``` + +This is the pattern used in most .NET MAUI handlers and other `UIView` subclasses to eliminate circular references. \ No newline at end of file diff --git a/.github/prompts/dotnet/maui/mct-maui-controls.prompt.md b/.github/prompts/dotnet/maui/mct-maui-controls.prompt.md new file mode 100644 index 0000000000..401f0ad781 --- /dev/null +++ b/.github/prompts/dotnet/maui/mct-maui-controls.prompt.md @@ -0,0 +1,2135 @@ +# Community Toolkit Controls +By following these common code styles and implementation templates, you can ensure that code is consistent, maintainable, and adheres to best practices. Always review and test code to ensure it meets our project's standards and requirements. + +## UI Components and Controls +- **Good Practice**: Ensure that any UI components or controls are compatible with .NET MAUI. +- **Bad Practice**: Using Xamarin.Forms-specific code unless there is a direct .NET MAUI equivalent. + +```csharp +// Good Practice +public class CustomButton : Button +{ + // .NET MAUI specific implementation +} + +// Bad Practice +public class CustomButton : Xamarin.Forms.Button +{ + // Xamarin.Forms specific implementation +} +``` + +## Using CommunityToolkit.Maui +- **Good Practice**: Utilize `CommunityToolkit.Maui` for common behaviors, converters, and extensions. +- **Bad Practice**: Re-implementing functionality that is already provided by `CommunityToolkit.Maui`. + +```csharp +// Good Practice +using CommunityToolkit.Maui.Behaviors; + +public class MainPage : ContentPage +{ + public MainPage() + { + var entry = new Entry(); + entry.Behaviors.Add(new EventToCommandBehavior + { + EventName = "TextChanged", + Command = new Command(() => { /* Command implementation */ }) + }); + } +} + +// Bad Practice +public class MainPage : ContentPage +{ + public MainPage() + { + var entry = new Entry(); + entry.TextChanged += (s, e) => { /* Event handler implementation */ }; + } +} +``` + +## File Scopes Namespaces +- **Good Practice**: Use file-scoped namespaces to reduce code verbosity. +- **Bad Practice**: Using block-scoped namespaces. + +```csharp +// Good Practice +namespace CommunityToolkit.Maui.Views; + +using System; + +class AvatarView +{ +} + +// Bad Practice +namespace CommunityToolkit.Maui.Views +{ + using System; + + class AvatarView + { + } +} +``` + +## Alerts +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Alerts` + +### Alert Initialization +- **Good Practice**: Use dependency injection to initialize alert services. +- **Bad Practice**: Creating instances of alert services directly within the class. + +```csharp +// Good Practice +public class AlertService +{ + private readonly IAlertService _alertService; + + public AlertService(IAlertService alertService) + { + _alertService = alertService ?? throw new ArgumentNullException(nameof(alertService)); + } + + public async Task ShowAlertAsync(string message, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } +} + +// Bad Practice +public class AlertService +{ + private readonly IAlertService _alertService = new AlertService(); + + public async Task ShowAlertAsync(string message) + { + await _alertService.ShowAlertAsync(message); + } +} +``` + +### Handling Alert Display +- **Good Practice**: Implement proper error handling and logging when displaying alerts. +- **Bad Practice**: Not handling exceptions or logging errors. + +```csharp +// Good Practice +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error displaying alert: {ex.Message}"); + } +} + +// Bad Practice +public async Task ShowAlertAsync(string message) +{ + await _alertService.ShowAlertAsync(message); +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// +/// Displays an alert with the specified message. +/// +/// The message to display. +/// The cancellation token. +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error displaying alert: {ex.Message}"); + } +} + +// Bad Practice +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error displaying alert: {ex.Message}"); + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); +} + +// Bad Practice +public void ShowAlert(string message) +{ + _alertService.ShowAlert(message); +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } + catch (ArgumentNullException ex) + { + Trace.WriteLine($"ArgumentNullException: {ex.Message}"); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} + +// Bad Practice +public async Task ShowAlertAsync(string message, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _alertService.ShowAlertAsync(message, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} +``` + +## Animations +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Animations` + +### Animation Initialization +- **Good Practice**: Use dependency injection to initialize animation services. +- **Bad Practice**: Creating instances of animation services directly within the class. + +```csharp +// Good Practice +public class CustomAnimation +{ + private readonly IAnimationService _animationService; + + public CustomAnimation(IAnimationService animationService) + { + _animationService = animationService ?? throw new ArgumentNullException(nameof(animationService)); + } + + public async Task StartAnimationAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); + } +} + +// Bad Practice +public class CustomAnimation +{ + private readonly IAnimationService _animationService = new AnimationService(); + + public async Task StartAnimationAsync() + { + await _animationService.StartAsync(); + } +} +``` + +### Handling Animation Completion +- **Good Practice**: Implement proper error handling and logging when completing animations. +- **Bad Practice**: Not handling exceptions or logging errors. + +```csharp +// Good Practice +public async Task CompleteAnimationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _animationService.CompleteAsync(token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error completing animation: {ex.Message}"); + } +} + +// Bad Practice +public async Task CompleteAnimationAsync() +{ + await _animationService.CompleteAsync(); +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Starts the animation. +/// The cancellation token. +public async Task StartAnimationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error starting animation: {ex.Message}"); + } +} + +// Bad Practice +public async Task StartAnimationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error starting animation: {ex.Message}"); + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task StartAnimationAsync(CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); +} + +// Bad Practice +public void StartAnimation() +{ + _animationService.Start(); +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public async Task StartAnimationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); + } + catch (ArgumentNullException ex) + { + Trace.WriteLine($"ArgumentNullException: {ex.Message}"); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} + +// Bad Practice +public async Task StartAnimationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} +``` + +### Animation Configuration +- **Good Practice**: Use configuration objects to manage animation settings. +- **Bad Practice**: Hardcoding animation settings within the class. + +```csharp +// Good Practice +public class CustomAnimation +{ + private readonly IAnimationService _animationService; + private readonly AnimationConfig _config; + + public CustomAnimation(IAnimationService animationService, AnimationConfig config) + { + _animationService = animationService ?? throw new ArgumentNullException(nameof(animationService)); + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public async Task StartAnimationAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + await _animationService.StartAsync(_config, token); + } +} + +// Bad Practice +public class CustomAnimation +{ + private readonly IAnimationService _animationService = new AnimationService(); + + public async Task StartAnimationAsync() + { + var config = new AnimationConfig + { + Duration = TimeSpan.FromSeconds(1), + Easing = Easing.Linear + }; + await _animationService.StartAsync(config); + } +} +``` + +## Converters +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Converters` + +### Converter Implementation +- **Good Practice**: Implement `IValueConverter` or `IMultiValueConverter` for creating converters. +- **Bad Practice**: Not implementing the required interfaces or leaving methods unimplemented. + +```csharp +// Good Practice +public class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Visibility visibility) + { + return visibility == Visibility.Visible; + } + return false; + } +} + +// Bad Practice +public class BoolToVisibilityConverter +{ + // Missing interface implementation +} +``` + +### Null Checking +- **Good Practice**: Use `is` for null checking. +- **Bad Practice**: Using `==` for null checking. + +```csharp +// Good Practice +if (value is null) +{ + return Visibility.Collapsed; +} + +// Bad Practice +if (value == null) +{ + return Visibility.Collapsed; +} +``` + +### Type Checking +- **Good Practice**: Use `is` for type checking. +- **Bad Practice**: Using casting for type checking. + +```csharp +// Good Practice +if (value is bool boolValue) +{ + return boolValue ? Visibility.Visible : Visibility.Collapsed; +} + +// Bad Practice +var boolValue = value as bool?; +if (boolValue != null) +{ + return boolValue.Value ? Visibility.Visible : Visibility.Collapsed; +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Converts a boolean value to a visibility value. +/// The boolean value. +/// The target type. +/// The converter parameter. +/// The culture. +/// Visibility.Visible if true, otherwise Visibility.Collapsed. +public object Convert(object value, Type targetType, object parameter, CultureInfo culture) +{ + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; +} + +// Bad Practice +public object Convert(object value, Type targetType, object parameter, CultureInfo culture) +{ + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public object Convert(object value, Type targetType, object parameter, CultureInfo culture) +{ + try + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + throw new ArgumentException("Expected boolean value", nameof(value)); + } + catch (ArgumentException ex) + { + Trace.WriteLine($"ArgumentException: {ex.Message}"); + return Visibility.Collapsed; + } +} + +// Bad Practice +public object Convert(object value, Type targetType, object parameter, CultureInfo culture) +{ + try + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + throw new Exception("Invalid value"); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return Visibility.Collapsed; + } +} +``` + +### Naming Conventions +- **Good Practice**: Follow consistent naming conventions for methods, properties, and variables. +- **Bad Practice**: Using inconsistent or unclear names. + +```csharp +// Good Practice +public class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Visibility visibility) + { + return visibility == Visibility.Visible; + } + return false; + } +} + +// Bad Practice +public class BoolToVisConverter : IValueConverter +{ + public object Conv(object val, Type tgtType, object param, CultureInfo cult) + { + if (val is bool bVal) + { + return bVal ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvBack(object val, Type tgtType, object param, CultureInfo cult) + { + if (val is Visibility vis) + { + return vis == Visibility.Visible; + } + return false; + } +} +``` + +## Essentials +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Essentials` + +### Using Essentials APIs +- **Good Practice**: Use `CommunityToolkit.Maui.Essentials` APIs for accessing device features. +- **Bad Practice**: Using platform-specific code directly. + +```csharp +// Good Practice +public class DeviceInfoService +{ + public string GetDeviceModel() + { + return DeviceInfo.Model; + } +} + +// Bad Practice +public class DeviceInfoService +{ + public string GetDeviceModel() + { +#if ANDROID + return Android.OS.Build.Model; +#elif IOS + return UIKit.UIDevice.CurrentDevice.Model; +#endif + } +} +``` + +### Handling Permissions +- **Good Practice**: Use `Permissions` API to request and check permissions. +- **Bad Practice**: Not handling permissions or using platform-specific code. + +```csharp +// Good Practice +public async Task CheckAndRequestLocationPermissionAsync() +{ + var status = await Permissions.CheckStatusAsync(); + if (status != PermissionStatus.Granted) + { + status = await Permissions.RequestAsync(); + } + return status; +} + +// Bad Practice +public async Task CheckAndRequestLocationPermissionAsync() +{ +#if ANDROID + var status = ContextCompat.CheckSelfPermission(Android.App.Application.Context, Manifest.Permission.AccessFineLocation); + if (status != Permission.Granted) + { + ActivityCompat.RequestPermissions(MainActivity.Instance, new[] { Manifest.Permission.AccessFineLocation }, 0); + } + return status == Permission.Granted; +#elif IOS + var status = CLLocationManager.Status; + if (status != CLAuthorizationStatus.AuthorizedWhenInUse) + { + locationManager.RequestWhenInUseAuthorization(); + } + return status == CLAuthorizationStatus.AuthorizedWhenInUse; +#endif +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task GetLocationAsync(CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + return await Geolocation.GetLocationAsync(new GeolocationRequest(), token); +} + +// Bad Practice +public Location GetLocation() +{ + return Geolocation.GetLocationAsync(new GeolocationRequest()).Result; +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public async Task GetLocationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await Geolocation.GetLocationAsync(new GeolocationRequest(), token); + } + catch (FeatureNotSupportedException ex) + { + Trace.WriteLine($"FeatureNotSupportedException: {ex.Message}"); + return null; + } + catch (PermissionException ex) + { + Trace.WriteLine($"PermissionException: {ex.Message}"); + return null; + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return null; + } +} + +// Bad Practice +public async Task GetLocationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await Geolocation.GetLocationAsync(new GeolocationRequest(), token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return null; + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Gets the current location. +/// The cancellation token. +/// The current location. +public async Task GetLocationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await Geolocation.GetLocationAsync(new GeolocationRequest(), token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return null; + } +} + +// Bad Practice +public async Task GetLocationAsync(CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await Geolocation.GetLocationAsync(new GeolocationRequest(), token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return null; + } +} +``` + +### Using Dependency Injection +- **Good Practice**: Use dependency injection to manage dependencies and improve testability. +- **Bad Practice**: Creating instances of dependencies directly within the class. + +```csharp +// Good Practice +public class LocationService +{ + private readonly IGeolocation _geolocation; + + public LocationService(IGeolocation geolocation) + { + _geolocation = geolocation ?? throw new ArgumentNullException(nameof(geolocation)); + } + + public async Task GetLocationAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + return await _geolocation.GetLocationAsync(new GeolocationRequest(), token); + } +} + +// Bad Practice +public class LocationService +{ + private readonly IGeolocation _geolocation = new Geolocation(); + + public async Task GetLocationAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + return await _geolocation.GetLocationAsync(new GeolocationRequest(), token); + } +} +``` + +## Extensions +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Extensions` + +### Extension Method Implementation +- **Good Practice**: Implement extension methods in static classes and use the `this` keyword to specify the type being extended. +- **Bad Practice**: Implementing extension methods in non-static classes or not using the `this` keyword. + +```csharp +// Good Practice +public static class StringExtensions +{ + public static bool IsNullOrEmpty(this string value) + { + return string.IsNullOrEmpty(value); + } +} + +// Bad Practice +public class StringExtensions +{ + public bool IsNullOrEmpty(string value) + { + return string.IsNullOrEmpty(value); + } +} +``` + +### Naming Conventions +- **Good Practice**: Follow consistent naming conventions for extension methods. +- **Bad Practice**: Using inconsistent or unclear names. + +```csharp +// Good Practice +public static class StringExtensions +{ + public static bool IsNullOrEmpty(this string value) + { + return string.IsNullOrEmpty(value); + } +} + +// Bad Practice +public static class StringExtensions +{ + public static bool IsNull(this string value) + { + return string.IsNullOrEmpty(value); + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public extension methods. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Determines whether the specified string is null or empty. +/// The string to check. +/// true if the string is null or empty; otherwise, false. +public static bool IsNullOrEmpty(this string value) +{ + return string.IsNullOrEmpty(value); +} + +// Bad Practice +public static bool IsNullOrEmpty(this string value) +{ + return string.IsNullOrEmpty(value); +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public static int ToInt(this string value) +{ + try + { + return int.Parse(value); + } + catch (FormatException ex) + { + Trace.WriteLine($"FormatException: {ex.Message}"); + return 0; + } +} + +// Bad Practice +public static int ToInt(this string value) +{ + try + { + return int.Parse(value); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + return 0; + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous extension methods and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public static async Task DownloadStringAsync(this HttpClient client, string url, CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + return await client.GetStringAsync(url, token); +} + +// Bad Practice +public static string DownloadString(this HttpClient client, string url) +{ + return client.GetStringAsync(url).Result; +} +``` + +### Handling Null Values +- **Good Practice**: Check for null values and handle them appropriately. +- **Bad Practice**: Not checking for null values, leading to potential `NullReferenceException`. + +```csharp +// Good Practice +public static string ToUpperSafe(this string value) +{ + return value?.ToUpper() ?? string.Empty; +} + +// Bad Practice +public static string ToUpperSafe(this string value) +{ + return value.ToUpper(); +} +``` + +## Handler Implementation +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.HandlerImplementation` + +### Handler Initialization +- **Good Practice**: Use dependency injection to initialize handlers. +- **Bad Practice**: Creating instances of handlers directly within the class. + +```csharp +// Good Practice +public class CustomHandler +{ + private readonly IHandlerService _handlerService; + + public CustomHandler(IHandlerService handlerService) + { + _handlerService = handlerService ?? throw new ArgumentNullException(nameof(handlerService)); + } + + public void InitializeHandler() + { + _handlerService.Initialize(); + } +} + +// Bad Practice +public class CustomHandler +{ + private readonly IHandlerService _handlerService = new HandlerService(); + + public void InitializeHandler() + { + _handlerService.Initialize(); + } +} +``` + +### Handling Events +- **Good Practice**: Implement proper error handling and logging when handling events. +- **Bad Practice**: Not handling exceptions or logging errors. + +```csharp +// Good Practice +public void HandleEvent() +{ + try + { + // Event handling logic + } + catch (Exception ex) + { + Trace.WriteLine($"Error handling event: {ex.Message}"); + } +} + +// Bad Practice +public void HandleEvent() +{ + // Event handling logic +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Initializes the handler. +public void InitializeHandler() +{ + _handlerService.Initialize(); +} + +// Bad Practice +public void InitializeHandler() +{ + _handlerService.Initialize(); +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task HandleEventAsync(CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + await _handlerService.HandleEventAsync(token); +} + +// Bad Practice +public void HandleEvent() +{ + _handlerService.HandleEventAsync().Wait(); +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public void InitializeHandler() +{ + try + { + _handlerService.Initialize(); + } + catch (ArgumentNullException ex) + { + Trace.WriteLine($"ArgumentNullException: {ex.Message}"); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} + +// Bad Practice +public void InitializeHandler() +{ + try + { + _handlerService.Initialize(); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } +} +``` + +### Naming Conventions +- **Good Practice**: Follow consistent naming conventions for methods, properties, and variables. +- **Bad Practice**: Using inconsistent or unclear names. + +```csharp +// Good Practice +public class CustomHandler +{ + public void InitializeHandler() + { + _handlerService.Initialize(); + } +} + +// Bad Practice +public class CustomHandler +{ + public void InitHandler() + { + _handlerService.Initialize(); + } +} +``` + +## Image Sources +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.ImageSources` + +### Image Source Initialization +- **Good Practice**: Use dependency injection to initialize image sources. +- **Bad Practice**: Creating instances of image sources directly within the class. + +```csharp +// Good Practice +public class CustomImageSource +{ + private readonly IImageService _imageService; + + public CustomImageSource(IImageService imageService) + { + _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); + } + + public async Task GetImageSourceAsync(string imageUrl, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + return await _imageService.LoadImageAsync(imageUrl, token); + } +} + +// Bad Practice +public class CustomImageSource +{ + private readonly IImageService _imageService = new ImageService(); + + public async Task GetImageSourceAsync(string imageUrl) + { + return await _imageService.LoadImageAsync(imageUrl); + } +} +``` + +### Handling Image Loading +- **Good Practice**: Implement proper error handling and logging when loading images. +- **Bad Practice**: Not handling exceptions or logging errors. + +```csharp +// Good Practice +public async Task GetImageSourceAsync(string imageUrl, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await _imageService.LoadImageAsync(imageUrl, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error loading image: {ex.Message}"); + return null; + } +} + +// Bad Practice +public async Task GetImageSourceAsync(string imageUrl) +{ + return await _imageService.LoadImageAsync(imageUrl); +} +``` + +### Caching Image Sources +- **Good Practice**: Use caching mechanisms to improve performance and reduce network usage. +- **Bad Practice**: Not implementing caching for frequently used images. + +```csharp +// Good Practice +public class CachedImageSource +{ + private readonly IImageService _imageService; + private readonly IMemoryCache _cache; + + public CachedImageSource(IImageService imageService, IMemoryCache cache) + { + _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + } + + public async Task GetImageSourceAsync(string imageUrl, CancellationToken token = default) + { + if (_cache.TryGetValue(imageUrl, out ImageSource cachedImage)) + { + return cachedImage; + } + + token.ThrowIfCancellationRequested(); + var imageSource = await _imageService.LoadImageAsync(imageUrl, token); + _cache.Set(imageUrl, imageSource); + return imageSource; + } +} + +// Bad Practice +public class CachedImageSource +{ + private readonly IImageService _imageService; + + public CachedImageSource(IImageService imageService) + { + _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); + } + + public async Task GetImageSourceAsync(string imageUrl) + { + return await _imageService.LoadImageAsync(imageUrl); + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// +/// Loads an image from the specified URL. +/// +/// The URL of the image. +/// The cancellation token. +/// The loaded image source. +public async Task GetImageSourceAsync(string imageUrl, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await _imageService.LoadImageAsync(imageUrl, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error loading image: {ex.Message}"); + return null; + } +} + +// Bad Practice +public async Task GetImageSourceAsync(string imageUrl, CancellationToken token = default) +{ + try + { + token.ThrowIfCancellationRequested(); + return await _imageService.LoadImageAsync(imageUrl, token); + } + catch (Exception ex) + { + Trace.WriteLine($"Error loading image: {ex.Message}"); + return null; + } +} +``` + +## Layouts +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Layouts` + +### Layout Initialization +- **Good Practice**: Use constructors to initialize layout properties and set up child elements. +- **Bad Practice**: Leaving constructors empty or not initializing necessary properties. + +```csharp +// Good Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + Spacing = 10; + Padding = new Thickness(5); + Children.Add(new Label { Text = "Hello, World!" }); + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + // Empty constructor + } +} +``` + +### Handling Layout Changes +- **Good Practice**: Override `OnSizeAllocated` to handle layout changes and adjust child elements accordingly. +- **Bad Practice**: Not handling layout changes or using event handlers for layout changes. + +```csharp +// Good Practice +public class CustomLayout : StackLayout +{ + protected override void OnSizeAllocated(double width, double height) + { + base.OnSizeAllocated(width, height); + // Adjust child elements based on new size + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + SizeChanged += OnSizeChanged; + } + + private void OnSizeChanged(object sender, EventArgs e) + { + // Adjust child elements based on new size + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// +/// Custom layout that arranges child elements in a stack. +/// +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + Spacing = 10; + Padding = new Thickness(5); + Children.Add(new Label { Text = "Hello, World!" }); + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + Spacing = 10; + Padding = new Thickness(5); + Children.Add(new Label { Text = "Hello, World!" }); + } +} +``` + +### Using Bindable Properties +- **Good Practice**: Use `BindableProperty.Create` to define bindable properties with appropriate default values and property changed callbacks. +- **Bad Practice**: Not providing default values or property changed callbacks. + +```csharp +// Good Practice +public class CustomLayout : StackLayout +{ + public static readonly BindableProperty SpacingProperty = BindableProperty.Create( + nameof(Spacing), + typeof(double), + typeof(CustomLayout), + defaultValue: 10.0, + propertyChanged: OnSpacingPropertyChanged); + + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + private static void OnSpacingPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + var layout = (CustomLayout)bindable; + layout.Spacing = (double)newValue; + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public static readonly BindableProperty SpacingProperty = BindableProperty.Create( + nameof(Spacing), + typeof(double), + typeof(CustomLayout)); + + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } +} +``` + +### Handling Null Values +- **Good Practice**: Check for null values and handle them appropriately. +- **Bad Practice**: Not checking for null values, leading to potential `NullReferenceException`. + +```csharp +// Good Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + var label = new Label { Text = "Hello, World!" }; + if (label != null) + { + Children.Add(label); + } + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public CustomLayout() + { + var label = new Label { Text = "Hello, World!" }; + Children.Add(label); + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public class CustomLayout : StackLayout +{ + public async Task LoadDataAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + // Asynchronous operation + } +} + +// Bad Practice +public class CustomLayout : StackLayout +{ + public void LoadData() + { + // Synchronous operation + } +} +``` + +## Platform Configuration +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.PlatformConfiguration` + +### Platform-Specific Configuration +- **Good Practice**: Use platform-specific directives to implement platform-specific configurations. +- **Bad Practice**: Mixing platform-specific code without directives. + +```csharp +// Good Practice +public static class StatusBarConfiguration +{ + public static void SetStatusBarColor(Color color) + { +#if ANDROID + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif + } +} + +// Bad Practice +public static class StatusBarConfiguration +{ + public static void SetStatusBarColor(Color color) + { + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); + + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); + } +} +``` + +### Using Dependency Injection +- **Good Practice**: Use dependency injection to manage platform-specific services. +- **Bad Practice**: Creating instances of platform-specific services directly within the class. + +```csharp +// Good Practice +public class PlatformService +{ + private readonly IPlatformSpecificService _platformSpecificService; + + public PlatformService(IPlatformSpecificService platformSpecificService) + { + _platformSpecificService = platformSpecificService ?? throw new ArgumentNullException(nameof(platformSpecificService)); + } + + public void PerformOperation() + { + _platformSpecificService.PerformOperation(); + } +} + +// Bad Practice +public class PlatformService +{ + private readonly IPlatformSpecificService _platformSpecificService = new PlatformSpecificService(); + + public void PerformOperation() + { + _platformSpecificService.PerformOperation(); + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Sets the status bar color. +/// The color to set. +public static void SetStatusBarColor(Color color) +{ +#if ANDROID + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif +} + +// Bad Practice +public static void SetStatusBarColor(Color color) +{ +#if ANDROID + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif +} +``` + +### Handling Platform Differences +- **Good Practice**: Use platform-specific extensions to handle differences in platform implementations. +- **Bad Practice**: Using conditional compilation without encapsulating platform-specific logic. + +```csharp +// Good Practice +public static class PlatformExtensions +{ + public static void SetStatusBarColor(this Window window, Color color) + { +#if ANDROID + window.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif + } +} + +// Bad Practice +public static class StatusBarConfiguration +{ + public static void SetStatusBarColor(Color color) + { +#if ANDROID + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public static async Task SetStatusBarColorAsync(Color color, CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); +#if ANDROID + var activity = Platform.CurrentActivity; + await Task.Run(() => activity?.Window?.SetStatusBarColor(color.ToPlatformColor()), token); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + await Task.Run(() => statusBar?.SetBackgroundColor(color.ToPlatformColor()), token); +#endif +} + +// Bad Practice +public static void SetStatusBarColor(Color color) +{ +#if ANDROID + var activity = Platform.CurrentActivity; + activity?.Window?.SetStatusBarColor(color.ToPlatformColor()); +#elif IOS + var statusBar = UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) as UIView; + statusBar?.SetBackgroundColor(color.ToPlatformColor()); +#endif +} +``` + +## Popup +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Popup` + +### Popup Initialization +- **Good Practice**: Use constructors to initialize popup properties and set up child elements. +- **Bad Practice**: Leaving constructors empty or not initializing necessary properties. + +```csharp +// Good Practice +public class CustomPopup : Popup +{ + public CustomPopup() + { + Content = new Label { Text = "Hello, World!" }; + CanBeDismissedByTappingOutsideOfPopup = true; + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + public CustomPopup() + { + // Empty constructor + } +} +``` + +### Handling Popup Events +- **Good Practice**: Implement event handlers for popup events such as appearing and disappearing. +- **Bad Practice**: Not handling events or leaving event handlers empty. + +```csharp +// Good Practice +public class CustomPopup : Popup +{ + public CustomPopup() + { + Opened += OnPopupOpened; + Closed += OnPopupClosed; + } + + private void OnPopupOpened(object? sender, EventArgs e) + { + // Handle popup opened event + } + + private void OnPopupClosed(object? sender, EventArgs e) + { + // Handle popup closed event + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + public CustomPopup() + { + Opened += OnPopupOpened; + Closed += OnPopupClosed; + } + + private void OnPopupOpened(object? sender, EventArgs e) + { + // Empty handler + } + + private void OnPopupClosed(object? sender, EventArgs e) + { + // Empty handler + } +} +``` + +### XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Custom popup that displays a message. +public class CustomPopup : Popup +{ + public CustomPopup() + { + Content = new Label { Text = "Hello, World!" }; + CanBeDismissedByTappingOutsideOfPopup = true; + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + public CustomPopup() + { + Content = new Label { Text = "Hello, World!" }; + CanBeDismissedByTappingOutsideOfPopup = true; + } +} +``` + +### Using Async/Await +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public class CustomPopup : Popup +{ + public async Task ShowPopupAsync(CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + await ShowAsync(token); + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + public void ShowPopup() + { + Show(); + } +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +public class CustomPopup : Popup +{ + public async Task ShowPopupAsync(CancellationToken token = default) + { + try + { + token.ThrowIfCancellationRequested(); + await ShowAsync(token); + } + catch (ArgumentNullException ex) + { + Trace.WriteLine($"ArgumentNullException: {ex.Message}"); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + public async Task ShowPopupAsync(CancellationToken token = default) + { + try + { + token.ThrowIfCancellationRequested(); + await ShowAsync(token); + } + catch (Exception ex) + { + Trace.WriteLine($"Exception: {ex.Message}"); + } + } +} +``` + +### Dependency Injection +- **Good Practice**: Use dependency injection to manage dependencies and improve testability. +- **Bad Practice**: Creating instances of dependencies directly within the class. + +```csharp +// Good Practice +public class CustomPopup : Popup +{ + private readonly ILoggingService _loggingService; + + public CustomPopup(ILoggingService loggingService) + { + _loggingService = loggingService ?? throw new ArgumentNullException(nameof(loggingService)); + Content = new Label { Text = "Hello, World!" }; + } +} + +// Bad Practice +public class CustomPopup : Popup +{ + private readonly ILoggingService _loggingService = new LoggingService(); + + public CustomPopup() + { + Content = new Label { Text = "Hello, World!" }; + } +} +``` + +## Views +Common Code Styles and Implementation Templates for `CommunityToolkit.Maui.Views` + +### Bindable Properties +- **Good Practice**: Use `BindableProperty.Create` to define bindable properties with appropriate default values and property changed callbacks. +- **Bad Practice**: Not providing default values or property changed callbacks. + +```csharp +// Good Practice +public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( + nameof(BorderColor), + typeof(Color), + typeof(AvatarView), + defaultValue: AvatarViewDefaults.DefaultBorderColor, + propertyChanged: OnBorderColorPropertyChanged); + +// Bad Practice +public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( + nameof(BorderColor), + typeof(Color), + typeof(AvatarView)); +``` + +### Property Changed Callbacks +- **Good Practice**: Implement property changed callbacks to handle changes in bindable properties. +- **Bad Practice**: Not implementing property changed callbacks or leaving them empty. + +```csharp +// Good Practice +static void OnBorderColorPropertyChanged(BindableObject bindable, object oldValue, object newValue) +{ + AvatarView avatarView = (AvatarView)bindable; + avatarView.Stroke = (Color)newValue; +} + +// Bad Practice +static void OnBorderColorPropertyChanged(BindableObject bindable, object oldValue, object newValue) +{ + // Empty callback +} +``` + +### Constructor Initialization +- **Good Practice**: Initialize properties and set up bindings in the constructor. +- **Bad Practice**: Leaving the constructor empty or not initializing necessary properties. + +```csharp +// Good Practice +public AvatarView() +{ + PropertyChanged += HandlePropertyChanged; + + IsEnabled = true; + HorizontalOptions = VerticalOptions = LayoutOptions.Center; + HeightRequest = AvatarViewDefaults.DefaultHeightRequest; + WidthRequest = AvatarViewDefaults.DefaultWidthRequest; + Padding = AvatarViewDefaults.DefaultPadding; + Stroke = AvatarViewDefaults.DefaultBorderColor; + StrokeThickness = AvatarViewDefaults.DefaultBorderWidth; + StrokeShape = new RoundRectangle + { + CornerRadius = new CornerRadius(AvatarViewDefaults.DefaultCornerRadius.TopLeft, AvatarViewDefaults.DefaultCornerRadius.TopRight, AvatarViewDefaults.DefaultCornerRadius.BottomLeft, AvatarViewDefaults.DefaultCornerRadius.BottomRight), + }; + Content = avatarLabel; + avatarImage.SetBinding(WidthRequestProperty, BindingBase.Create(static p => p.WidthRequest, source: this)); + avatarImage.SetBinding(HeightRequestProperty, BindingBase.Create(static p => p.HeightRequest, source: this)); +} + +// Bad Practice +public AvatarView() +{ + // Empty constructor +} +``` + +### Event Handling +- **Good Practice**: Implement event handlers for property changes and other events. +- **Bad Practice**: Not handling events or leaving event handlers empty. + +```csharp +// Good Practice +void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e) +{ + if ((e.PropertyName == HeightProperty.PropertyName + || e.PropertyName == WidthProperty.PropertyName + || e.PropertyName == PaddingProperty.PropertyName + || e.PropertyName == ImageSourceProperty.PropertyName + || e.PropertyName == BorderWidthProperty.PropertyName + || e.PropertyName == CornerRadiusProperty.PropertyName + || e.PropertyName == StrokeThicknessProperty.PropertyName) + && Height >= 0 + && Width >= 0 + && avatarImage.Source is not null) + { + // Handle property changes + } +} + +// Bad Practice +void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e) +{ + // Empty handler +} +``` + +### Interface Implementations +- **Good Practice**: Implement interfaces and their members properly. +- **Bad Practice**: Not implementing required interface members or leaving them unimplemented. + +```csharp +// Good Practice +Aspect IImageElement.Aspect => avatarImage.Aspect; +bool IImageElement.IsLoading => avatarImage.IsLoading; + +// Bad Practice +Aspect IImageElement.Aspect => throw new NotImplementedException(); +bool IImageElement.IsLoading => throw new NotImplementedException(); +``` + +### Debug Logging +- **Good Practice**: Use `Trace.WriteLine()` for debug logging. +- **Bad Practice**: Using `Debug.WriteLine()` which is removed by the compiler in Release builds. + +```csharp +// Good Practice +Trace.WriteLine("Debug message"); + +// Bad Practice +Debug.WriteLine("Debug message"); +``` + +### Methods Returning Task and ValueTask +- **Good Practice**: Include a `CancellationToken` as a parameter for methods returning `Task` or `ValueTask`. +- **Bad Practice**: Not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task LoadDataAsync(CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + // Method implementation +} + +// Bad Practice +public async Task LoadDataAsync() +{ + // Method implementation +} +``` + +### Pattern Matching +- **Good Practice**: Use `is` for null checking and type checking. +- **Bad Practice**: Using `==` for null checking or casting for type checking. + +```csharp +// Good Practice +if (something is null) +{ + // Handle null +} + +if (something is Bucket bucket) +{ + bucket.Empty(); +} + +// Bad Practice +if (something == null) +{ + // Handle null +} + +var bucket = something as Bucket; +if (bucket != null) +{ + bucket.Empty(); +} +``` + +### Dependency Injection +- **Good Practice**: Use dependency injection to manage dependencies and improve testability. +- **Bad Practice**: Creating instances of dependencies directly within the class. + +```csharp +// Good Practice +public class AvatarView +{ + private readonly IImageService _imageService; + + public AvatarView(IImageService imageService) + { + _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); + } +} + +// Bad Practice +public class AvatarView +{ + private readonly IImageService _imageService = new ImageService(); +} +``` + +### Async/Await Usage +- **Good Practice**: Use `async` and `await` for asynchronous operations and include a `CancellationToken` parameter. +- **Bad Practice**: Using synchronous methods for operations that can be asynchronous or not including a `CancellationToken`. + +```csharp +// Good Practice +public async Task LoadDataAsync(CancellationToken token = default) +{ + token.ThrowIfCancellationRequested(); + // Asynchronous operation +} + +// Bad Practice +public void LoadData() +{ + // Synchronous operation +} +``` + +### Exception Handling +- **Good Practice**: Use specific exception types and provide meaningful messages. +- **Bad Practice**: Catching general exceptions or not providing meaningful messages. + +```csharp +// Good Practice +try +{ + // Code that may throw an exception +} +catch (ArgumentNullException ex) +{ + Trace.WriteLine($"ArgumentNullException: {ex.Message}"); +} + +// Bad Practice +try +{ + // Code that may throw an exception +} +catch (Exception ex) +{ + Trace.WriteLine($"Exception: {ex.Message}"); +} +``` + +## XML Documentation +- **Good Practice**: Provide XML documentation for public members. +- **Bad Practice**: Not providing documentation or leaving it empty. + +```csharp +// Good Practice +/// Gets or sets the border color. +public Color BorderColor +{ + get => (Color)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); +} + +// Bad Practice +public Color BorderColor +{ + get => (Color)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); +} +``` + +## Naming Conventions +- **Good Practice**: Follow consistent naming conventions for methods, properties, and variables. +- **Bad Practice**: Using inconsistent or unclear names. + +```csharp +// Good Practice +public void LoadUserData() +{ + // Method implementation +} + +// Bad Practice +public void load_user_data() +{ + // Method implementation +} +``` \ No newline at end of file diff --git a/.github/prompts/dotnet/testing.xunit.prompt.md b/.github/prompts/dotnet/testing.xunit.prompt.md new file mode 100644 index 0000000000..85cc387b51 --- /dev/null +++ b/.github/prompts/dotnet/testing.xunit.prompt.md @@ -0,0 +1,393 @@ +# Unit Testing +The default unit testing framework for .NET projects is xUnit. This document provides guidelines for writing unit tests using xUnit. + +Tests should be reliable, maintainable, provide meaningful coverage, and provide proper isolation and clear patterns for test organization and execution. + +## Requirements +- Use xUnit as the testing framework +- Ensure test isolation +- Follow consistent patterns +- Maintain high code coverage + +## Test Class Structure: + +- Use ITestOutputHelper for logging: + ```csharp + public class OnControlBindingContextChanged(ITestOutputHelper output) + { + + [Fact] + public async Task BindingContext_Changed() + { + output.WriteLine("Starting test with default binding context"); + // Test implementation + } + } + ``` +- Use fixtures for shared state: + ```csharp + public class DatabaseFixture : IAsyncLifetime + { + public DbConnection Connection { get; private set; } + + public async Task InitializeAsync() + { + Connection = new SqlConnection("connection-string"); + await Connection.OpenAsync(); + } + + public async Task DisposeAsync() + { + await Connection.DisposeAsync(); + } + } + + public class OrderTests : IClassFixture + { + private readonly DatabaseFixture _fixture; + private readonly ITestOutputHelper _output; + + public OrderTests(DatabaseFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + } + } + ``` + +## Test Methods: + +- Prefer Theory over multiple Facts: + ```csharp + public class DiscountCalculatorTests + { + public static TheoryData TestData { get; } = new() + { + // System.Byte + { + Convert.ToByte('C'), Convert.ToByte('B'), Convert.ToByte('D'), TrueTestObject, FalseTestObject, TrueTestObject + }, + { + Convert.ToByte('B'), Convert.ToByte('B'), Convert.ToByte('D'), TrueTestObject, FalseTestObject, TrueTestObject + }, + { + Convert.ToByte('D'), Convert.ToByte('B'), Convert.ToByte('D'), TrueTestObject, FalseTestObject, TrueTestObject + }, + }; + + [Theory] + [MemberData(nameof(TestData))] + public void IsInRangeConverterConvertFrom(IComparable value, IComparable comparingMinValue, IComparable comparingMaxValue, object trueObject, object falseObject, object expectedResult) + { + // Arrange + IsInRangeConverter isInRangeConverter = new() + { + MinValue = comparingMinValue, + MaxValue = comparingMaxValue, + FalseObject = falseObject, + TrueObject = trueObject, + }; + + // Act + object? convertResult = ((ICommunityToolkitValueConverter)isInRangeConverter).Convert(value, typeof(object), null, CultureInfo.CurrentCulture); + object convertFromResult = isInRangeConverter.ConvertFrom(value, CultureInfo.CurrentCulture); + + // Assert + Assert.Equal(expectedResult, convertResult); + Assert.Equal(expectedResult, convertFromResult); + } + } + ``` +- Follow Arrange-Act-Assert pattern: + ```csharp + [Fact] + public void ViewStructure_CorrectNumberOfChildren() + { + // Arrange + const int maximumRating = 3; + RatingView ratingView = new() + { + MaximumRating = maximumRating + }; + + // Act + var controlTemplate = ratingView.ControlTemplate; + var visualTreeDescendantsCount = ratingView.RatingLayout.GetVisualTreeDescendants().Count; + var childrenCount = ratingView.RatingLayout.Children.Count; + + // Assert + Assert.NotNull(controlTemplate); + Assert.Equal((maximumRating * 2) + 1, visualTreeDescendantsCount); + Assert.Equal(maximumRating, childrenCount); + } + ``` + +## Test Isolation: + +- Use fresh data for each test: + ```csharp + public class OrderTests + { + private static Order CreateTestOrder() => + new(OrderId.New(), TestData.CreateOrderLines()); + + [Fact] + public async Task ProcessOrder_Success() + { + var order = CreateTestOrder(); + // Test implementation + } + } + ``` +- Clean up resources: + ```csharp + [Fact] + public void IsDisposedDisposeTokenSource() + { + // Arrange + Image testControl = new() + { + Source = new GravatarImageSource() + }; + + // Act + testControl.Layout(new Rect(0, 0, 37, 73)); + var gravatarImageSource = (GravatarImageSource)testControl.Source; + bool isDisposedBefore = gravatarImageSource.IsDisposed; + gravatarImageSource.Dispose(); + bool isDisposedAfter = gravatarImageSource.IsDisposed; + + // Assert + Assert.True(testControl.Source is GravatarImageSource); + Assert.False(isDisposedBefore); + Assert.True(isDisposedAfter); + } + ``` + +## Best Practices: + +- Name tests clearly: + ```csharp + // Good: Clear test names + [Fact] + public void ChangingEmailWithNoSizeDoesNotUpdateUri() + + // Avoid: Unclear names + [Fact] + public void ChangeEmail() + ``` +- Use meaningful assertions: + ```csharp + // Good: Clear assertions + Assert.Equal(expected, actual); + Assert.Contains(expectedItem, collection); + Assert.Throws(() => new RatingView().MaximumRating = 0); + + // Avoid: Multiple assertions without context + Assert.NotNull(result); + Assert.True(result.Success); + Assert.Equal(0, result.Errors.Count); + ``` +- Handle async operations properly: + ```csharp + // Good: Async test method + [Fact] + public async Task ProcessOrder_ValidOrder_Succeeds() + { + await using var processor = new OrderProcessor(); + var result = await processor.ProcessAsync(order); + Assert.True(result.IsSuccess); + } + + // Avoid: Sync over async + [Fact] + public void ProcessOrder_ValidOrder_Succeeds() + { + using var processor = new OrderProcessor(); + var result = processor.ProcessAsync(order).Result; // Can deadlock + Assert.True(result.IsSuccess); + } + ``` +- Use `TestContext.Current.CancellationToken` for cancellation: + ```csharp + // Good: + [Fact] + public async Task ProcessOrder_CancellationRequested() + { + await using var processor = new OrderProcessor(); + var result = await processor.ProcessAsync(order, TestContext.Current.CancellationToken); + Assert.True(result.IsSuccess); + } + // Avoid: + [Fact] + public async Task ProcessOrder_CancellationRequested() + { + await using var processor = new OrderProcessor(); + var result = await processor.ProcessAsync(order, CancellationToken.None); + Assert.False(result.IsSuccess); + } + ``` + +## Assertions: + +- Use xUnit's built-in assertions: + ```csharp + // Good: Using xUnit's built-in assertions + public class OrderTests + { + [Fact] + public void CalculateTotal_WithValidLines_ReturnsCorrectSum() + { + // Arrange + var order = new Order( + OrderId.New(), + new[] + { + new OrderLine("SKU1", 2, 10.0m), + new OrderLine("SKU2", 1, 20.0m) + }); + + // Act + var total = order.CalculateTotal(); + + // Assert + Assert.Equal(40.0m, total); + } + + [Fact] + public void Order_WithInvalidLines_ThrowsException() + { + // Arrange + var invalidLines = new OrderLine[] { }; + + // Act & Assert + var ex = Assert.Throws(() => + new Order(OrderId.New(), invalidLines)); + Assert.Equal("Order must have at least one line", ex.Message); + } + + [Fact] + public void Order_WithValidData_HasExpectedProperties() + { + // Arrange + var id = OrderId.New(); + var lines = new[] { new OrderLine("SKU1", 1, 10.0m) }; + + // Act + var order = new Order(id, lines); + + // Assert + Assert.NotNull(order); + Assert.Equal(id, order.Id); + Assert.Single(order.Lines); + Assert.Collection(order.Lines, + line => + { + Assert.Equal("SKU1", line.Sku); + Assert.Equal(1, line.Quantity); + Assert.Equal(10.0m, line.Price); + }); + } + } + ``` + +- Avoid third-party assertion libraries: + ```csharp + // Avoid: Using FluentAssertions or similar libraries + public class OrderTests + { + [Fact] + public void CalculateTotal_WithValidLines_ReturnsCorrectSum() + { + var order = new Order( + OrderId.New(), + new[] + { + new OrderLine("SKU1", 2, 10.0m), + new OrderLine("SKU2", 1, 20.0m) + }); + + // Avoid: Using FluentAssertions + order.CalculateTotal().Should().Be(40.0m); + order.Lines.Should().HaveCount(2); + order.Should().NotBeNull(); + } + } + ``` + +- Use proper assertion types: + ```csharp + public class CustomerTests + { + [Fact] + public void Customer_WithValidEmail_IsCreated() + { + // Boolean assertions + Assert.True(customer.IsActive); + Assert.False(customer.IsDeleted); + + // Equality assertions + Assert.Equal("john@example.com", customer.Email); + Assert.NotEqual(Guid.Empty, customer.Id); + + // Collection assertions + Assert.Empty(customer.Orders); + Assert.Contains("Admin", customer.Roles); + Assert.DoesNotContain("Guest", customer.Roles); + Assert.All(customer.Orders, o => Assert.NotNull(o.Id)); + + // Type assertions + Assert.IsType(customer); + Assert.IsAssignableFrom(customer); + + // String assertions + Assert.StartsWith("CUST", customer.Reference); + Assert.Contains("Premium", customer.Description); + Assert.Matches("^CUST\\d{6}$", customer.Reference); + + // Range assertions + Assert.InRange(customer.Age, 18, 100); + + // Reference assertions + Assert.Same(expectedCustomer, actualCustomer); + Assert.NotSame(differentCustomer, actualCustomer); + } + } + ``` + +- Use Assert.Collection for complex collections: + ```csharp + [Fact] + public void ProcessOrder_CreatesExpectedEvents() + { + // Arrange + var processor = new OrderProcessor(); + var order = CreateTestOrder(); + + // Act + var events = processor.Process(order); + + // Assert + Assert.Collection(events, + evt => + { + Assert.IsType(evt); + var received = Assert.IsType(evt); + Assert.Equal(order.Id, received.OrderId); + }, + evt => + { + Assert.IsType(evt); + var reserved = Assert.IsType(evt); + Assert.Equal(order.Id, reserved.OrderId); + Assert.NotEmpty(reserved.ReservedItems); + }, + evt => + { + Assert.IsType(evt); + var confirmed = Assert.IsType(evt); + Assert.Equal(order.Id, confirmed.OrderId); + Assert.True(confirmed.IsSuccess); + }); + } + ``` \ No newline at end of file diff --git a/.github/prompts/prompts.prompt.md b/.github/prompts/prompts.prompt.md new file mode 100644 index 0000000000..b8ff6ae65a --- /dev/null +++ b/.github/prompts/prompts.prompt.md @@ -0,0 +1,10 @@ +# Prompt Instructions + +Include these prompt instructions if their link's name is called. + +- [MAUI Controls](dotnet/maui/maui-controls.prompt.md) +- [MAUI Memory Leaks](dotnet/maui/maui-memory-leaks.prompt.md) +- [Async Await](dotnet/async.prompt.md) +- [Code Style](dotnet/codestyle.prompt.md) +- [MCT Controls](dotnet/maui/mct-maui-controls.prompt.md) +- [Testing using xUnit](dotnet/testing.xunit.prompt.md) \ No newline at end of file