diff --git a/Atc.Wpf.sln b/Atc.Wpf.sln index d5aed653..0216002f 100644 --- a/Atc.Wpf.sln +++ b/Atc.Wpf.sln @@ -40,6 +40,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atc.Wpf.SourceGenerators", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atc.Wpf.SourceGenerators.Tests", "test\Atc.Wpf.SourceGenerators.Tests\Atc.Wpf.SourceGenerators.Tests.csproj", "{6B9C0EC5-1D71-4B95-96C8-E5A7FCDF8276}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + docs\SourceGenerators\AttachedProperty.md = docs\SourceGenerators\AttachedProperty.md + docs\SourceGenerators\DependencyProperty.md = docs\SourceGenerators\DependencyProperty.md + docs\SourceGenerators\ViewModel.md = docs\SourceGenerators\ViewModel.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mvvm", "Mvvm", "{02E4711C-F1F1-4682-8111-47957F772DDD}" + ProjectSection(SolutionItems) = preProject + docs\Mvvm\@Readme.md = docs\Mvvm\@Readme.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,9 +123,11 @@ Global {F4BE59B5-26CB-46DB-B290-2049EF656693} = {0FA723AA-8BA7-42AD-9C08-6491F364A258} {104F4D6B-3991-4FBA-9234-0B37DF6BB292} = {0FA723AA-8BA7-42AD-9C08-6491F364A258} {6B9C0EC5-1D71-4B95-96C8-E5A7FCDF8276} = {2C893B25-2401-44B7-9FDA-7DC496C00D2B} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {133AFD8A-A870-41D7-8A97-763A36D2502A} + {02E4711C-F1F1-4682-8111-47957F772DDD} = {133AFD8A-A870-41D7-8A97-763A36D2502A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7A92D7EC-F444-456B-A86C-D6683E016C35} RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationTail"]} + SolutionGuid = {7A92D7EC-F444-456B-A86C-D6683E016C35} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 42eed778..ab6fa269 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,23 @@ This is a base libraries for building WPF application with the MVVM design patte - [Table of contents](#table-of-contents) - [Requirements](#requirements) - [NuGet Packages Provided in this Repository](#nuget-packages-provided-in-this-repository) - - [Demonstration Application](#demonstration-application) - - [Playground and Viewer for a Given Control or Functionality](#playground-and-viewer-for-a-given-control-or-functionality) +- [πŸ”Ž Demonstration Application](#-demonstration-application) + - [Playground and Viewer for a Given Control or Functionality](#playground-and-viewer-for-a-given-control-or-functionality) - [Initial glimpse at the demonstration application](#initial-glimpse-at-the-demonstration-application) - - [How to get started with atc-wpf](#how-to-get-started-with-atc-wpf) - - [Readme's for each NuGet Package area](#readmes-for-each-nuget-package-area) - - [Atc.Wpf](#atcwpf) - - [Controls](#controls) - - [Misc](#misc) - - [Atc.Wpf.Controls](#atcwpfcontrols) - - [Controls](#controls-1) - - [Misc](#misc-1) - - [Atc.Wpf.FontIcons](#atcwpffonticons) - - [Misc](#misc-2) - - [Atc.Wpf.Theming](#atcwpftheming) - - [How to contribute](#how-to-contribute) +- [πŸš€ How to get started with atc-wpf](#-how-to-get-started-with-atc-wpf) + - [WPF with MVVM Easily Separate UI and Business Logic](#wpf-with-mvvm-easily-separate-ui-and-business-logic) +- [πŸ“ Readme's for each NuGet Package area](#-readmes-for-each-nuget-package-area) + - [πŸ’Ÿ Atc.Wpf](#-atcwpf) + - [Controls](#controls) + - [Misc](#misc) + - [πŸ’Ÿ Atc.Wpf.Controls](#-atcwpfcontrols) + - [Controls](#controls-1) + - [Misc](#misc-1) + - [πŸ’Ÿ Atc.Wpf.FontIcons](#-atcwpffonticons) + - [Misc](#misc-2) + - [πŸ’Ÿ Atc.Wpf.Theming](#-atcwpftheming) +- [βš™οΈ Source Generators](#️-source-generators) +- [How to contribute](#how-to-contribute) ## Requirements @@ -34,19 +36,19 @@ This is a base libraries for building WPF application with the MVVM design patte | Nuget package | Description | Dependencies | |-------------------------|-----------------------------------------------------|--------------------------------| -| Atc.Wpf | Base Controls, ValueConverters, Extensions etc. | Atc & Atc.Wpf.SourceGenerators | -| Atc.Wpf.Controls | Miscellaneous UI Controls | Atc.Wpf & Atc.Wpf.Theming | -| Atc.Wpf.Controls.Sample | Controls for creating WPF sample apps | Atc.Wpf & Atc.Wpf.Theming | -| Atc.Wpf.FontIcons | Render Svg and Img resources based on fonts | Atc.Wpf | -| Atc.Wpf.Theming | Theming for Light & Dark mode for WPF base controls | Atc.Wpf | +| πŸ’Ÿ Atc.Wpf | Base Controls, ValueConverters, Extensions etc. | Atc & Atc.Wpf.SourceGenerators | +| πŸ’Ÿ Atc.Wpf.Controls | Miscellaneous UI Controls | Atc.Wpf & Atc.Wpf.Theming | +| πŸ’Ÿ Atc.Wpf.Controls.Sample | Controls for creating WPF sample apps | Atc.Wpf & Atc.Wpf.Theming | +| πŸ’Ÿ Atc.Wpf.FontIcons | Render Svg and Img resources based on fonts | Atc.Wpf | +| πŸ’Ÿ Atc.Wpf.Theming | Theming for Light & Dark mode for WPF base controls | Atc.Wpf | -## Demonstration Application +# πŸ”Ž Demonstration Application The demonstration application, `Atc.Wpf.Sample`, functions as a control explorer. It provides quick visualization of a given control, along with options for copying and pasting the XAML markup and/or the C# code for how to use it. -### Playground and Viewer for a Given Control or Functionality +## Playground and Viewer for a Given Control or Functionality The following example is taken from the ReplayCommandAsync which illustrates its usage: @@ -70,16 +72,16 @@ The following example is taken from the ReplayCommandAsync which illustrates its | Wpf.Theming - ImageButton ![Img](docs/images/lm-wpf-theming-imagebutton.png) | Wpf.Theming - ImageButton ![Img](docs/images/dm-wpf-theming-imagebutton.png) | | Wpf.FontIcons - Viewer ![Img](docs/images/lm-wpf-fonicons-viewer.png) | Wpf.FontIcons - Viewer ![Img](docs/images/dm-wpf-fonicons-viewer.png) | -## How to get started with atc-wpf +# πŸš€ How to get started with atc-wpf First of all, include Nuget packages in the `.csproj` file like this: ```xml - - - - + + + + ``` @@ -105,126 +107,78 @@ Then update `App.xaml` like this: Now it is possible to use controls with theming and default WPF controls like TextBox, Button etc. with theme style. -## Readme's for each NuGet Package area +## WPF with MVVM Easily Separate UI and Business Logic + +With the `Atc.Wpf`, package, it is very easy to get startet with the nice `MVVM pattern` + +Please read more here: + +- [MVVM framework](docs/Mvvm/@Readme.md) + - [Observerble properties](docs/Mvvm/@Readme.md) + - [RelayCommands](docs/Mvvm/@Readme.md) + +# πŸ“ Readme's for each NuGet Package area ***Note: Right now, it is a limit amount of controls and components there is documented with a `Readme.md` file. -Therefore run the `Atc.Wpf.Sample` application to explore all the controls and components.*** :-) +Therefore run the `Atc.Wpf.Sample` application to explore all the controls and components.*** 😊 -### Atc.Wpf +## πŸ’Ÿ Atc.Wpf -#### Controls +### Controls -- [GridEx](src/Atc.Wpf/Controls/Layouts/GridEx_Readme.md) -- [StaggeredPanel](src/Atc.Wpf/Controls/Layouts/StaggeredPanel_Readme.md) -- [UniformSpacingPanel](src/Atc.Wpf/Controls/Layouts/UniformSpacingPanel_Readme.md) -- [SvgImage](src/Atc.Wpf/Controls/Media/SvgImage_Readme.md) -- Control Helpers +- Layouts + - [GridEx](src/Atc.Wpf/Controls/Layouts/GridEx_Readme.md) + - [StaggeredPanel](src/Atc.Wpf/Controls/Layouts/StaggeredPanel_Readme.md) + - [UniformSpacingPanel](src/Atc.Wpf/Controls/Layouts/UniformSpacingPanel_Readme.md) +- Media + - [SvgImage](src/Atc.Wpf/Controls/Media/SvgImage_Readme.md) +- Helpers - [PanelHelper](src/Atc.Wpf/Helpers/PanelHelper_Readme.md) -#### Misc +### Misc -- [MVVM framework](src/Atc.Wpf/Mvvm/@Readme.md) - - [RelayCommand's](src/Atc.Wpf/Command/@Readme.md) - [ShaderEffects](src/Atc.Wpf/Media/ShaderEffects/@Readme.md) - [How to use HLSL Shader Compiler](src/Atc.Wpf/Media/ShaderEffects/Shaders/@Readme.md) - [Tranlation & localizaion](src/Atc.Wpf/Translation/@Readme.md) - [ValueConverters](src/Atc.Wpf/ValueConverters/@Readme.md) -### Atc.Wpf.Controls +## πŸ’Ÿ Atc.Wpf.Controls -#### Controls +### Controls - [WellKnownColorPicker](src/Atc.Wpf.Controls/ColorControls/WellKnownColorPicker_Readme.md) -#### Misc +### Misc - [ValueConverters](src/Atc.Wpf.Controls/ValueConverters/@Readme.md) -### Atc.Wpf.FontIcons +## πŸ’Ÿ Atc.Wpf.FontIcons -#### Misc +### Misc - [ValueConverters](src/Atc.Wpf.FontIcons/ValueConverters/@Readme.md) -### Atc.Wpf.Theming +## πŸ’Ÿ Atc.Wpf.Theming - [ValueConverters](src/Atc.Wpf.Theming/ValueConverters/@Readme.md) -### Source Generators +# βš™οΈ Source Generators -In MVVM, certain attributes help automate boilerplate code using source generators. +In for WPF, certain attributes help automate boilerplate code using source generators. -Example for ViewModel classes +Read more about here: -![MVVM Source Generation](docs/images/mvvm-source-generated.png) - -For more details, see the [MVVM](src/Atc.Wpf/Mvvm/@Readme.md) section. +- [SourceGenerators for AttachedProperties](docs/SourceGenerators/AttachedProperty.md) +- [SourceGenerators for DependencyProperties](docs/SourceGenerators/DependencyProperty.md) +- [SourceGenerators for ViewModel](docs/SourceGenerators/ViewModel.md) -### DependencyProperty +Example for ViewModel classes -### Human made Code for complex property - -```csharp -[DependencyProperty("IsRunning")] -public partial class MyControl : UserControl -{ -} -``` - -### Human made Code for simple property - -```csharp -[DependencyProperty("IsRunning"] -public partial class MyControl : UserControl -{ -} -``` - -### Generated Code for simple property - -```csharp -public partial class MyControl -{ - public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register( - nameof(IsRunning), - typeof(bool), - typeof(MyControl), - new FrameworkPropertyMetadata(defaultValue: BooleanBoxes.TrueBox); - - public bool IsRunning - { - get => (bool)GetValue(IsRunningProperty); - set => SetValue(IsRunningProperty, value); - } -} -``` +![MVVM Source Generation](docs/images/mvvm-source-generated.png) -### Generated Code for complex property - -```csharp -public partial class MyControl -{ - public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register( - nameof(IsRunning), - typeof(bool), - typeof(MyControl), - new FrameworkPropertyMetadata( - defaultValue: BooleanBoxes.TrueBox, - propertyChangedCallback: PropertyChangedCallback, - coerceValueCallback: CoerceValueCallback, - flags: FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, - defaultUpdateSourceTrigger: UpdateSourceTrigger.Default, - isAnimationProhibited: true)); - - public bool IsRunning - { - get => (bool)GetValue(IsRunningProperty); - set => SetValue(IsRunningProperty, value); - } -} -``` +For more details, see the [MVVM](docs/Mvvm/@Readme.md) section. -## How to contribute +# How to contribute [Contribution Guidelines](https://atc-net.github.io/introduction/about-atc#how-to-contribute) diff --git a/docs/Mvvm/@Readme.md b/docs/Mvvm/@Readme.md new file mode 100644 index 00000000..6ae0fe9d --- /dev/null +++ b/docs/Mvvm/@Readme.md @@ -0,0 +1,50 @@ +# MVVM in WPF + +Windows Presentation Foundation (WPF) fully supports the **Model-View-ViewModel (MVVM)** pattern, which promotes a clear separation of concerns between the UI and business logic. + +The **Atc.Wpf** library provides a robust foundation for implementing MVVM effectively, reducing boilerplate code and simplifying development. + +## Features + +The `Atc.Wpf` library offers a variety of base classes and utilities to streamline MVVM implementation: + +| Component | Description | +|---------------------------|--------------------------------------------------------------------------------| +| `ViewModelBase` | A base class for ViewModels. | +| `MainWindowViewModelBase` | A base class for the main window ViewModel. | +| `ViewModelDialogBase` | A base class for dialog ViewModels. | +| `ObservableObject` | A base class for observable objects implementing `INotifyPropertyChanged`. | +| `RelayCommand` | A command supporting `CanExecute`. | +| `RelayCommand` | A command with a generic parameter and `CanExecute`. | +| `RelayCommandAsync` | An asynchronous command supporting `CanExecute`. | +| `RelayCommandAsync` | An asynchronous command with a generic parameter and `CanExecute`. | + +For detailed information about commands, refer to the [RelayCommand documentation](../SourceGenerators/ViewModel.md). + +--- + +### Getting started using `ViewModelBase` + +Below is a simple example demonstrating how to create a ViewModel using `ViewModelBase`: + +```csharp +public class MyViewModel : ViewModelBase +{ + private string myProperty; + + public string MyProperty + { + get => myProperty; + set + { + if (myProperty == value) + { + return; + } + + myProperty = value; + RaisePropertyChanged(); + } + } +} +``` diff --git a/docs/SourceGenerators/AttachedProperty.md b/docs/SourceGenerators/AttachedProperty.md new file mode 100644 index 00000000..fba6c3f2 --- /dev/null +++ b/docs/SourceGenerators/AttachedProperty.md @@ -0,0 +1,207 @@ +# βš™οΈ AttachedProperty with SourceGeneration + +In WPF, **attached properties** are a type of dependency property that allows properties to be defined in one class but used in another. They are widely used in scenarios like behaviors, layout configurations, and interactions where a property needs to be applied to multiple elements without modifying their class definitions. Traditionally, defining attached properties requires boilerplate code, but source generators can automate this process, reducing errors and improving maintainability. + +--- + +## πŸš€ Defining an Attached Property + +### ✨ Creating a Simple Attached Property + +Let's define an attached property using source generators. + +```csharp +[AttachedProperty("IsDraggable")] +public static partial class DragBehavior +{ +} +``` + +### πŸ” What's Happening Here? + +- The `AttachedPropertyAttribute` automatically generates the `IsDraggable` property with `Get` and `Set` methods. + +### πŸ–₯️ XAML Example + +```xml + + + + + +``` + +This allows the `IsDraggable` property to be applied to any UI element dynamically. + +--- + +## πŸ“Œ Summary + +This example showcases `advanced metadata` for attached properties, allowing: + +- βœ”οΈ **Automatic property registration** +- βœ”οΈ **Flexible application to various UI elements** +- βœ”οΈ **Custom property value coercion and validation** +- βœ”οΈ **Efficient UI updates** +- βœ”οΈ **Simplified code structure** + +### πŸš€ Why Use Atc.Wpf Source Generators? + +- βœ… **Eliminates boilerplate** – Just declare the property, and the generator handles the rest. +- βœ… **Ensures consistency** – Less room for human error. + +--- + +## πŸ”Ž Behind the scenes + +### πŸ“ Human-Written Code - for simple example + +```csharp +[AttachedProperty("IsDraggable"] +public static partial class DragBehavior +{ +} +``` + +In this example: + +- The `[AttachedProperty("IsDraggable")]` attribute declares an attached property named `IsDraggable` of type `bool` for the `DragBehavior` class. +- The source generator will automatically create the necessary methods and property registration. + +### βš™οΈ Auto-Generated Code - for simple example + +The source generator will produce code equivalent to: + +```csharp +public static partial class DragBehavior +{ + public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached( + "IsDraggable", + typeof(bool), + typeof(DragBehavior), + new PropertyMetadata(defaultValue: BooleanBoxes.FalseBox)); + + public static bool GetIsDraggable(UIElement element) + => element is not null && (bool)element.GetValue(IsDraggableProperty); + + public static void SetIsDraggable(UIElement element, bool value) + => element?.SetValue(IsDraggableProperty, value); +} +``` + +### πŸ“ Human-Written Code - for complex example + +```csharp +[AttachedProperty( + "IsDraggable", + DefaultValue = false, + PropertyChangedCallback = nameof(PropertyChangedCallback), + CoerceValueCallback = nameof(CoerceValueCallback), + Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default, + IsAnimationProhibited = true, + ValidateValueCallback = nameof(ValidateValueCallback))] +public partial class DragBehavior +{ + private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + throw new NotImplementedException(); + } + + private static object CoerceValueCallback(DependencyObject d, object baseValue) + { + throw new NotImplementedException(); + } + + private static bool ValidateValueCallback(object value) + { + throw new NotImplementedException(); + } +} +``` + +**In this example:** + +- `[AttachedProperty("IsDraggable")]` + - Declares a **dependency property** named `IsDraggable` of type `bool` for the `DragBehavior` class. + - Unlike a regular dependency property, an **attached property** is **not tied to a single class** but can be applied to any **UI element**. + - The source generator will automatically create: + - A `DependencyProperty` field for `IsDraggableProperty`. + - **Static** `GetIsDraggable` **and** `SetIsDraggable` **methods**, allowing other controls to use this property dynamically. + +- `DefaultValue = false` + - Specifies the default value of `IsDraggable` as `false`, meaning that **elements are not draggable unless explicitly enabled**. + +- `PropertyChangedCallback = nameof(PropertyChangedCallback)` + - Assigns a **property changed callback method**, that is triggered **whenever the** `IsDraggable` **value changes**. + - This allows dynamic behavior updatesβ€”e.g., adding or removing event handlers for drag operations. + +- `CoerceValueCallback = nameof(CoerceValueCallback)` + - Called before the property value is assigned. + - This method can **modify the value before applying it**. + - Example: If an element **must not be draggable** under certain conditions, the `CoerceValueCallback` can force the value back to `false`. + +- `Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender` + - Specifies that changes to `IsDraggable` affect the UI layout and rendering. + - `AffectsMeasure`: Triggers a re-measure of the UI element if this property changes. + - `AffectsRender`: Causes a re-render of the control when this property is modified. + - Example: If `IsDraggable` changes, **drag indicators or visual cues might need updating**. + +- `FrameworkPropertyMetadataOptions` + - A flag-based enumeration used to specify additional **property behavior** in WPF. + - Other possible values include: + - `AffectsParentMeasure`: Causes a layout pass on the parent when the property changes. + - `AffectsArrange`: Forces an arrange pass when the property changes. + - `Inherits`: Allows the property value to propagate down the visual tree. + - `BindsTwoWayByDefault`: Sets the default binding mode to **TwoWay**. + - These flags **optimize performance** by ensuring layout changes only occur when necessary. + +- `DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default` + - Specifies how **data binding** updates the property's source. + - The `Default` value means that the **default behavior of the property type** is used. + - Other possible values: + - `PropertyChanged`: Updates the source immediately when the property changes. + - `LostFocus`: Updates the source when the control loses focus (e.g., leaving a text box). + - `Explicit`: Requires manual invocation of `BindingExpression.UpdateSource()`. + +- `IsAnimationProhibited = true` + - Prevents animations from affecting this property. + - Some dependency properties allow animations to change their values smoothly over time. + - By setting `IsAnimationProhibited = true`, you ensure that **no animations** can modify `IsDraggable`. + - This is useful for properties where **instant updates are required**, such as boolean state changes. + +- `ValidateValueCallback = nameof(ValidateValueCallback)` + - Assigns a **validation callback method**, which ensures that only valid values are assigned to the dependency property. + - This function **executes before** the property value is set, allowing you to **reject invalid values** before they are applied. + - The `ValidateValueCallback` method should return a `bool`: + - `true`: The value is accepted and applied to the property. + - `false`: The value is considered invalid, and an exception is thrown. + +### βš™οΈ Auto-Generated Code - for complex example + +The source generator will produce equivalent code: + +```csharp +public partial class DragBehavior +{ + public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.Register( + nameof(IsDraggable), + typeof(bool), + typeof(DragBehavior), + new FrameworkPropertyMetadata( + defaultValue: BooleanBoxes.FalseBox, + propertyChangedCallback: PropertyChangedCallback, + coerceValueCallback: CoerceValueCallback, + flags: FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + defaultUpdateSourceTrigger: UpdateSourceTrigger.Default, + isAnimationProhibited: true, + validateValueCallback = ValidateValueCallback)); + + public bool IsDraggable + { + get => (bool)GetValue(IsDraggableProperty); + set => SetValue(IsDraggableProperty, value); + } +} +``` diff --git a/docs/SourceGenerators/DependencyProperty.md b/docs/SourceGenerators/DependencyProperty.md new file mode 100644 index 00000000..e2a90db9 --- /dev/null +++ b/docs/SourceGenerators/DependencyProperty.md @@ -0,0 +1,219 @@ +# βš™οΈ DependencyProperties with SourceGeneration + +In WPF, **dependency properties** are a specialized type of property that extends the functionality of standard CLR properties. They support features such as data binding, animation, and property value inheritance, which are integral to the WPF property system. However, defining dependency properties traditionally involves verbose boilerplate code. To streamline this process, source generators can automatically generate the necessary code, reducing errors and improving maintainability. + +--- + +## πŸš€ Setting Up Your First UserControl + +### ✨ Creating a Simple UserControl + +Let's start by defining a UserControl using source generators. + +```csharp +[DependencyProperty("IsRunning")] +public partial class TestView +{ + public TestView() + { + InitializeComponent(); + } +} +``` + +### πŸ” What's Happening Here? + +- The `DependencyPropertyAttribute` automatically generates the `IsRunning` property, including `INotifyPropertyChanged` support. + +### πŸ–₯️ XAML Binding Example + +```xml + + + + + + + + + + + +``` + +This setup allows the UI to dynamically update when the IsRunning property changes. + +--- + +## πŸ“Œ Summary + +This example showcases **advanced metadata** for dependency properties, allowing: + +- βœ”οΈ **Automatic property change notifications** +- βœ”οΈ **Value coercion and validation** +- βœ”οΈ **Optimized UI performance with layout invalidation** +- βœ”οΈ **Flexible data binding behavior** +- βœ”οΈ **Control over animation support** + +### πŸš€ Why Use Atc.Wpf Source Generators? + +- βœ… **Eliminates boilerplate** – Just declare the property, and the generator handles the rest. + +--- + +## πŸ”Ž Behind the scenes + +### πŸ“ Human-Written Code - for simple example + +```csharp +[DependencyProperty("IsRunning"] +public partial class MyControl : UserControl +{ +} +``` + +**In this example:** + +- The `[DependencyProperty("IsRunning")]` attribute indicates that a dependency property named `IsRunning` of type `bool` should be generated for the `MyControl` class. + +### βš™οΈ Auto-Generated Code - for simple example + +The source generator will produce code equivalent to: + +```csharp +public partial class MyControl +{ + public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register( + nameof(IsRunning), + typeof(bool), + typeof(MyControl), + new FrameworkPropertyMetadata(defaultValue: BooleanBoxes.TrueBox); + + public bool IsRunning + { + get => (bool)GetValue(IsRunningProperty); + set => SetValue(IsRunningProperty, value); + } +} +``` + +### πŸ“ Human-Written Code - for complex example + +```csharp + +[DependencyProperty( + "IsRunning", + DefaultValue = false, + PropertyChangedCallback = nameof(PropertyChangedCallback), + CoerceValueCallback = nameof(CoerceValueCallback), + Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default, + IsAnimationProhibited = true, + ValidateValueCallback = nameof(ValidateValueCallback))] +public partial class MyControl : UserControl +{ + private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + throw new NotImplementedException(); + } + + private static object CoerceValueCallback(DependencyObject d, object baseValue) + { + throw new NotImplementedException(); + } + + private static bool ValidateValueCallback(object value) + { + throw new NotImplementedException(); + } +} +``` + +**In this example:** + +- `[DependencyProperty("IsRunning")]` + - Declares a **dependency property** named `IsRunning` of type `bool` for `MyControl`. + - The source generator will automatically create a `DependencyProperty` field and a CLR property wrapper. + +- `DefaultValue = false` + - Specifies the default value of `IsRunning` as `false`. + - This means that when an instance of `MyControl` is created, `IsRunning` will be `false` unless explicitly set. + +- `PropertyChangedCallback = nameof(PropertyChangedCallback)` + - Assigns a **property changed callback method**, which is invoked whenever the property's value changes. + - In this example, `PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)` is defined as a static method. + - This method allows you to **respond to property changes**, such as triggering UI updates or executing business logic. + +- `CoerceValueCallback = nameof(CoerceValueCallback)` + - Assigns a **coerce value callback method**, which is called before setting the property’s value. + - This function allows validation, restricting the range of acceptable values, or adjusting the value based on other conditions. + - For instance, if `IsRunning` should never be `true` under specific circumstances, the `CoerceValueCallback` could enforce that rule. + +- `Flags = FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender` + - Configures how the property affects the **WPF layout system** when it changes. + - `AffectsMeasure`: Triggers a re-measure of the UI element if this property changes. + - `AffectsRender`: Causes a re-render of the control when this property is modified. + - These options ensure that any UI elements depending on `IsRunning` will **recalculate their size and appearance** accordingly. + +- `FrameworkPropertyMetadataOptions` + - A flag-based enumeration used to specify additional **property behavior** in WPF. + - Other possible values include: + - `AffectsParentMeasure`: Causes a layout pass on the parent when the property changes. + - `AffectsArrange`: Forces an arrange pass when the property changes. + - `Inherits`: Allows the property value to propagate down the visual tree. + - `BindsTwoWayByDefault`: Sets the default binding mode to **TwoWay**. + - These flags **optimize performance** by ensuring layout changes only occur when necessary. + +- `DefaultUpdateSourceTrigger = UpdateSourceTrigger.Default` + - Specifies how **data binding** updates the property's source. + - The `Default` value means that the **default behavior of the property type** is used. + - Other possible values: + - `PropertyChanged`: Updates the source immediately when the property changes. + - `LostFocus`: Updates the source when the control loses focus (e.g., leaving a text box). + - `Explicit`: Requires manual invocation of `BindingExpression.UpdateSource()`. + +- `IsAnimationProhibited = true` + - Prevents animations from affecting this property. + - Some dependency properties allow animations to change their values smoothly over time. + - By setting `IsAnimationProhibited = true`, you ensure that **no animations** can modify `IsRunning`. + - This is useful for properties where **instant updates are required**, such as boolean state changes. + +- `ValidateValueCallback = nameof(ValidateValueCallback)` + - Assigns a **validation callback method**, which ensures that only valid values are assigned to the dependency property. + - This function **executes before** the property value is set, allowing you to **reject invalid values** before they are applied. + - The `ValidateValueCallback` method should return a `bool`: + - `true`: The value is accepted and applied to the property. + - `false`: The value is considered invalid, and an exception is thrown. + +--- + +### βš™οΈ Auto-Generated Code - for complex example + +The source generator will produce code equivalent to: + +```csharp +public partial class MyControl +{ + public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register( + nameof(IsRunning), + typeof(bool), + typeof(MyControl), + new FrameworkPropertyMetadata( + defaultValue: BooleanBoxes.FalseBox, + propertyChangedCallback: PropertyChangedCallback, + coerceValueCallback: CoerceValueCallback, + flags: FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, + defaultUpdateSourceTrigger: UpdateSourceTrigger.Default, + isAnimationProhibited: true, + validateValueCallback = ValidateValueCallback)); + + public bool IsRunning + { + get => (bool)GetValue(IsRunningProperty); + set => SetValue(IsRunningProperty, value); + } +} +``` diff --git a/src/Atc.Wpf/Mvvm/@Readme.md b/docs/SourceGenerators/ViewModel.md similarity index 54% rename from src/Atc.Wpf/Mvvm/@Readme.md rename to docs/SourceGenerators/ViewModel.md index 35794bdb..31667ded 100644 --- a/src/Atc.Wpf/Mvvm/@Readme.md +++ b/docs/SourceGenerators/ViewModel.md @@ -1,55 +1,55 @@ -# MVVM +# βš™οΈ ViewModel with Source Generation -The Windows Presentation Foundation (WPF) fully supports the Model-View-ViewModel (MVVM) pattern. +The **Atc.Wpf Source Generators** simplify ViewModel development by reducing boilerplate code for properties and commands. With attributes like `ObservableProperty` and `RelayCommand`, you can focus on business logic while automatically handling property change notifications and command implementations. -The `Atc.Wpf` library provides a solid foundation for implementing MVVM effectively. - -## Features - -| Component | Description | -|---------------------------|--------------------------------------------------------------------------------| -| `ViewModelBase` | A base class for ViewModels. | -| `MainWindowViewModelBase` | A base class for the main window ViewModel. | -| `ViewModelDialogBase` | A base class for dialog ViewModels. | -| `ObservableObject` | A base class for observable objects implementing `INotifyPropertyChanged`. | -| `RelayCommand` | A command supporting `CanExecute`. | -| `RelayCommand` | A command with a generic parameter and `CanExecute`. | -| `RelayCommandAsync` | An asynchronous command supporting `CanExecute`. | -| `RelayCommandAsync` | An asynchronous command with a generic parameter and `CanExecute`. | +--- -For more details on commands, see the [RelayCommand documentation](../Command/@Readme.md). +## πŸš€ Setting Up Your First ViewModel ---- +### ✨ Creating a Simple ViewModel -## Example: Using `ViewModelBase` +Let's start by defining a ViewModel using source generators. ```csharp -public class MyViewModel : ViewModelBase +public partial class TestViewModel : ViewModelBase { - private string myProperty; - - public string MyProperty - { - get => myProperty; - set - { - if (myProperty == value) - { - return; - } - - myProperty = value; - RaisePropertyChanged(); - } - } + [ObservableProperty] + private string name; } ``` +### πŸ” What's Happening Here? + +- `ObservablePropertyAttribute` automatically generates the `Name` property, including `INotifyPropertyChanged` support. +- `RelayCommand` generates a `SayHelloCommand`, which can be bound to a button in the UI. + +### πŸ–₯️ XAML Binding Example + +```xml + + + + + + + + + + + + + @@ -56,6 +81,13 @@ ScrollViewer.CanContentScroll="True" SelectedItemChanged="TreeViewOnSelectionChanged" /> + + + public partial class MainWindow { + private IMainWindowViewModel GetViewModel() => (IMainWindowViewModel)DataContext!; + + private readonly TreeView[] sampleTreeViews; + public MainWindow(IMainWindowViewModel viewModel) { InitializeComponent(); + DataContext = viewModel; Loaded += OnLoaded; Closing += OnClosing; KeyDown += OnKeyDown; KeyUp += OnKeyUp; + + sampleTreeViews = + [ + StvSampleWpf, + StvSampleWpfControls, + StvSamplesWpfSourceGeneratorsTreeView, + StvSampleWpfFontIcons, + StvSampleWpfTheming, + ]; } private void OnLoaded( object sender, RoutedEventArgs e) { - var vm = DataContext as IMainWindowViewModel; - vm!.OnLoaded(this, e); - + GetViewModel().OnLoaded(this, e); Keyboard.Focus(TbSampleFilter); } private void OnClosing( object? sender, CancelEventArgs e) - { - var vm = DataContext as IMainWindowViewModel; - vm!.OnClosing(this, e); - } + => GetViewModel().OnClosing(this, e); private void OnKeyDown( object sender, KeyEventArgs e) - { - var vm = DataContext as IMainWindowViewModel; - vm!.OnKeyDown(this, e); - } + => GetViewModel().OnKeyDown(this, e); private void OnKeyUp( object sender, KeyEventArgs e) - { - var vm = DataContext as IMainWindowViewModel; - vm!.OnKeyUp(this, e); - } + => GetViewModel().OnKeyUp(this, e); private void TreeViewOnSelectionChanged( object sender, RoutedPropertyChangedEventArgs e) - { - var vm = DataContext as IMainWindowViewModel; - vm!.UpdateSelectedView(e.NewValue as SampleTreeViewItem); - } + => GetViewModel().UpdateSelectedView(e.NewValue as SampleTreeViewItem); private void SampleFilterOnTextChanged( object sender, @@ -67,10 +67,10 @@ private void SampleFilterOnTextChanged( return; } - _ = SetVisibilityByFilterTreeViewItems(StvSampleWpf.Items, textBox.Text); - _ = SetVisibilityByFilterTreeViewItems(StvSampleWpfControls.Items, textBox.Text); - _ = SetVisibilityByFilterTreeViewItems(StvSampleWpfFontIcons.Items, textBox.Text); - _ = SetVisibilityByFilterTreeViewItems(StvSampleWpfTheming.Items, textBox.Text); + foreach (var sampleTreeView in sampleTreeViews) + { + _ = SetVisibilityByFilterTreeViewItems(sampleTreeView.Items, textBox.Text); + } } private static bool SetVisibilityByFilterTreeViewItems( @@ -107,4 +107,38 @@ private static bool SetVisibilityByFilterTreeViewItems( return showRoot; } + + private void SampleExpandAll( + object sender, + RoutedEventArgs e) + => ProcessTreeViewItems(expand: true); + + private void SampleCollapseAll( + object sender, + RoutedEventArgs e) + => ProcessTreeViewItems(expand: false); + + private void ProcessTreeViewItems( + bool expand) + { + foreach (var treeView in sampleTreeViews) + { + foreach (TreeViewItem item in treeView.Items) + { + SetTreeViewItemExpansion(item, expand); + } + } + } + + private static void SetTreeViewItemExpansion( + TreeViewItem treeViewItem, + bool expand) + { + treeViewItem.IsExpanded = expand; + + foreach (TreeViewItem item in treeViewItem.Items) + { + SetTreeViewItemExpansion(item, expand); + } + } } \ No newline at end of file diff --git a/sample/Atc.Wpf.Sample/SamplesWpf/ControlAttach/MyAttach.cs b/sample/Atc.Wpf.Sample/SamplesWpf/ControlAttach/MyAttach.cs new file mode 100644 index 00000000..eec63833 --- /dev/null +++ b/sample/Atc.Wpf.Sample/SamplesWpf/ControlAttach/MyAttach.cs @@ -0,0 +1,47 @@ +namespace Atc.Wpf.Sample.SamplesWpf.ControlAttach; + +[AttachedProperty("Circular", PropertyChangedCallback = nameof(OnCircularChanged))] +public static partial class MyAttach +{ + private static void OnCircularChanged( + DependencyObject d, + DependencyPropertyChangedEventArgs e) + { + if (d is not System.Windows.Shapes.Rectangle rectangle) + { + return; + } + + if ((bool)e.NewValue) + { + var binding = new MultiBinding + { + Converter = new RectangleCircularValueConverter(), + }; + + binding.Bindings.Add(new Binding(FrameworkElement.ActualWidthProperty.Name) { Source = rectangle }); + binding.Bindings.Add(new Binding(FrameworkElement.ActualHeightProperty.Name) { Source = rectangle }); + rectangle.SetBinding(System.Windows.Shapes.Rectangle.RadiusXProperty, binding); + rectangle.SetBinding(System.Windows.Shapes.Rectangle.RadiusYProperty, binding); + } + else + { + BindingOperations.ClearBinding(rectangle, FrameworkElement.ActualWidthProperty); + BindingOperations.ClearBinding(rectangle, FrameworkElement.ActualHeightProperty); + BindingOperations.ClearBinding(rectangle, System.Windows.Shapes.Rectangle.RadiusXProperty); + BindingOperations.ClearBinding(rectangle, System.Windows.Shapes.Rectangle.RadiusYProperty); + } + } + + public static void DemoMethod() + { + // uiElement is just for demo + var uiElement = new Control(); + + // This method is generated by the AttachedPropertyAttribute + _ = GetCircular(uiElement); + + // This method is generated by the AttachedPropertyAttribute + SetCircular(uiElement, value: true); + } +} \ No newline at end of file diff --git a/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml b/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml new file mode 100644 index 00000000..b4f7f898 --- /dev/null +++ b/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml.cs b/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml.cs new file mode 100644 index 00000000..5262fb86 --- /dev/null +++ b/sample/Atc.Wpf.Sample/SamplesWpf/ValueConverters/ValueConvertersView.xaml.cs @@ -0,0 +1,13 @@ +namespace Atc.Wpf.Sample.SamplesWpf.ValueConverters +{ + /// + /// Interaction logic for ValueConvertersView. + /// + public partial class ValueConvertersView + { + public ValueConvertersView() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/sample/Atc.Wpf.Sample/SamplesWpfSourceGenerators/AttachedPropertyView.xaml b/sample/Atc.Wpf.Sample/SamplesWpfSourceGenerators/AttachedPropertyView.xaml new file mode 100644 index 00000000..8a6b08d2 --- /dev/null +++ b/sample/Atc.Wpf.Sample/SamplesWpfSourceGenerators/AttachedPropertyView.xaml @@ -0,0 +1,63 @@ + + + + + + + +