diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 5bbc3d6915..875161d336 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -84,11 +84,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
- "RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
+ "RunToolsTests",
"ZipFiles"
]
}
@@ -123,11 +123,11 @@
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
- "RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
+ "RunToolsTests",
"ZipFiles"
]
}
diff --git a/Avalonia.sln b/Avalonia.sln
index e66b73de0e..b21df07628 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -244,8 +244,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -573,10 +579,22 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -643,7 +661,10 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+ {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+ {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props
index 4929578b60..a66bff4999 100644
--- a/build/SourceGenerators.props
+++ b/build/SourceGenerators.props
@@ -1,5 +1,10 @@
-
+
+ true
+ false
+
+
+
+
+
+
+
+
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 46f267ae17..40232947d9 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -220,16 +220,18 @@ partial class Build : NukeBuild
.Executes(() =>
{
RunCoreTest("Avalonia.Skia.RenderTests");
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (Parameters.IsRunningOnWindows)
RunCoreTest("Avalonia.Direct2D1.RenderTests");
});
- Target RunDesignerTests => _ => _
- .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
+ Target RunToolsTests => _ => _
+ .OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile)
.Executes(() =>
{
- RunCoreTest("Avalonia.DesignerSupport.Tests");
+ RunCoreTest("Avalonia.Generators.Tests");
+ if (Parameters.IsRunningOnWindows)
+ RunCoreTest("Avalonia.DesignerSupport.Tests");
});
Target RunLeakTests => _ => _
@@ -276,7 +278,7 @@ partial class Build : NukeBuild
Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
- .DependsOn(RunDesignerTests)
+ .DependsOn(RunToolsTests)
.DependsOn(RunHtmlPreviewerTests)
.DependsOn(RunLeakTests);
diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config
index d1c0408241..09f22ec527 100644
--- a/nukebuild/numerge.config
+++ b/nukebuild/numerge.config
@@ -11,6 +11,11 @@
"Id": "Avalonia.Build.Tasks",
"IgnoreMissingFrameworkBinaries": true,
"DoNotMergeDependencies": true
+ },
+ {
+ "Id": "Avalonia.Generators",
+ "IgnoreMissingFrameworkBinaries": true,
+ "DoNotMergeDependencies": true
}
]
}
diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj
index 4d0ed866a3..1d210172f0 100644
--- a/packages/Avalonia/Avalonia.csproj
+++ b/packages/Avalonia/Avalonia.csproj
@@ -6,11 +6,15 @@
-
+
all
true
TargetFramework=netstandard2.0
+
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 0e84b3d182..5125b42426 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -2,7 +2,8 @@
netstandard2.0;net6.0
true
- enable
+ enable
+ true
@@ -35,14 +36,5 @@
-
-
-
-
-
-
-
-
-
-
+
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
index 7db6d9d334..8944151385 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
@@ -1,11 +1,10 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
-using Avalonia.Markup.Xaml;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public class FlyoutsPage : UserControl
+ public partial class FlyoutsPage : UserControl
{
public FlyoutsPage()
{
@@ -28,11 +27,6 @@ namespace ControlCatalog.Pages
}
}
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
private void SetXamlTexts()
{
var bfxt = this.Get("ButtonFlyoutXamlText");
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
index f05e5fd033..f3a7647f8c 100644
--- a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
@@ -1,11 +1,9 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
using ControlCatalog.Models;
namespace ControlCatalog.Pages
{
- public class LabelsPage : UserControl
+ public partial class LabelsPage : UserControl
{
private Person? _person;
@@ -25,11 +23,6 @@ namespace ControlCatalog.Pages
};
}
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
public void DoSave()
{
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
index f9d0328d9a..a710cd7e5c 100644
--- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
@@ -1,18 +1,15 @@
-using System.Threading.Tasks;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public class RefreshContainerPage : UserControl
+ public partial class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
- this.InitializeComponent();
+ InitializeComponent();
_viewModel = new RefreshContainerViewModel();
@@ -27,10 +24,5 @@ namespace ControlCatalog.Pages
deferral.Complete();
}
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
}
}
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
index 11d0a5152e..aec13a18e3 100644
--- a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
@@ -1,19 +1,12 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public class RelativePanelPage : UserControl
+ public partial class RelativePanelPage : UserControl
{
public RelativePanelPage()
{
- this.InitializeComponent();
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
+ InitializeComponent();
}
}
}
diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs
index f0ae1a722d..5a0c4cba43 100644
--- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs
+++ b/samples/ControlCatalog/Pages/ThemePage.axaml.cs
@@ -1,35 +1,31 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
namespace ControlCatalog.Pages
{
- public class ThemePage : UserControl
+ public partial class ThemePage : UserControl
{
public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
public ThemePage()
{
- AvaloniaXamlLoader.Load(this);
+ InitializeComponent();
- var selector = this.FindControl("Selector")!;
- var themeVariantScope = this.FindControl("ThemeVariantScope")!;
-
- selector.Items = new[]
+ Selector.Items = new[]
{
ThemeVariant.Default,
ThemeVariant.Dark,
ThemeVariant.Light,
Pink
};
- selector.SelectedIndex = 0;
+ Selector.SelectedIndex = 0;
- selector.SelectionChanged += (_, _) =>
+ Selector.SelectionChanged += (_, _) =>
{
- if (selector.SelectedItem is ThemeVariant theme)
+ if (Selector.SelectedItem is ThemeVariant theme)
{
- themeVariantScope.RequestedThemeVariant = theme;
+ ThemeVariantScope.RequestedThemeVariant = theme;
}
};
}
diff --git a/samples/Generators.Sandbox/App.xaml b/samples/Generators.Sandbox/App.xaml
new file mode 100644
index 0000000000..8064eac3f5
--- /dev/null
+++ b/samples/Generators.Sandbox/App.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/App.xaml.cs b/samples/Generators.Sandbox/App.xaml.cs
new file mode 100644
index 0000000000..6118b3f177
--- /dev/null
+++ b/samples/Generators.Sandbox/App.xaml.cs
@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+using Generators.Sandbox.ViewModels;
+
+namespace Generators.Sandbox;
+
+public class App : Application
+{
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ var view = new Views.SignUpView
+ {
+ ViewModel = new SignUpViewModel()
+ };
+ view.Show();
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Controls/CustomTextBox.cs b/samples/Generators.Sandbox/Controls/CustomTextBox.cs
new file mode 100644
index 0000000000..68ee925986
--- /dev/null
+++ b/samples/Generators.Sandbox/Controls/CustomTextBox.cs
@@ -0,0 +1,10 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Styling;
+
+namespace Generators.Sandbox.Controls;
+
+public class CustomTextBox : TextBox, IStyleable
+{
+ Type IStyleable.StyleKey => typeof(TextBox);
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Controls/SignUpView.xaml b/samples/Generators.Sandbox/Controls/SignUpView.xaml
new file mode 100644
index 0000000000..c126f36f53
--- /dev/null
+++ b/samples/Generators.Sandbox/Controls/SignUpView.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Controls/SignUpView.xaml.cs b/samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
new file mode 100644
index 0000000000..c4cd1cdc1a
--- /dev/null
+++ b/samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using Generators.Sandbox.ViewModels;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using ReactiveUI.Validation.Formatters;
+
+namespace Generators.Sandbox.Controls;
+
+///
+/// This is a sample view class with typed x:Name references generated using
+/// .NET 5 source generators. The class has to be partial because x:Name
+/// references are living in a separate partial class file. See also:
+/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
+///
+public partial class SignUpView : ReactiveUserControl
+{
+ public SignUpView()
+ {
+ // The InitializeComponent method is also generated automatically
+ // and lives in the autogenerated part of the partial class.
+ InitializeComponent();
+ this.WhenActivated(disposables =>
+ {
+ this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
+ .DisposeWith(disposables);
+ this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
+ .DisposeWith(disposables);
+ this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
+ .DisposeWith(disposables);
+ this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
+ .DisposeWith(disposables);
+
+ this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
+ .DisposeWith(disposables);
+ this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
+ .DisposeWith(disposables);
+ this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
+ .DisposeWith(disposables);
+
+ var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
+ this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
+ .DisposeWith(disposables);
+
+ // The references to text boxes below are also auto generated.
+ // Use Ctrl+Click in order to view the generated sources.
+ UserNameTextBox.Text = "Joseph!";
+ PasswordTextBox.Text = "1234";
+ ConfirmPasswordTextBox.Text = "1234";
+ SignUpButtonDescription.Text = "Press the button below to sign up.";
+ });
+ }
+}
diff --git a/samples/Generators.Sandbox/Generators.Sandbox.csproj b/samples/Generators.Sandbox/Generators.Sandbox.csproj
new file mode 100644
index 0000000000..885e71af4e
--- /dev/null
+++ b/samples/Generators.Sandbox/Generators.Sandbox.csproj
@@ -0,0 +1,28 @@
+
+
+ Exe
+ net6.0
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Program.cs b/samples/Generators.Sandbox/Program.cs
new file mode 100644
index 0000000000..7e533965df
--- /dev/null
+++ b/samples/Generators.Sandbox/Program.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace Generators.Sandbox;
+
+internal static class Program
+{
+ public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+ private static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UseReactiveUI()
+ .UsePlatformDetect()
+ .LogToTrace();
+}
diff --git a/samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs b/samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
new file mode 100644
index 0000000000..06b6122088
--- /dev/null
+++ b/samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
@@ -0,0 +1,70 @@
+using System.Reactive;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using ReactiveUI.Validation.Helpers;
+
+namespace Generators.Sandbox.ViewModels;
+
+public class SignUpViewModel : ReactiveValidationObject
+{
+ private string _userName = string.Empty;
+ private string _password = string.Empty;
+ private string _confirmPassword = string.Empty;
+
+ public SignUpViewModel()
+ {
+ this.ValidationRule(
+ vm => vm.UserName,
+ name => !string.IsNullOrWhiteSpace(name),
+ "UserName is required.");
+
+ this.ValidationRule(
+ vm => vm.Password,
+ password => !string.IsNullOrWhiteSpace(password),
+ "Password is required.");
+
+ this.ValidationRule(
+ vm => vm.Password,
+ password => password?.Length > 2,
+ password => $"Password should be longer, current length: {password.Length}");
+
+ this.ValidationRule(
+ vm => vm.ConfirmPassword,
+ confirmation => !string.IsNullOrWhiteSpace(confirmation),
+ "Confirm password field is required.");
+
+ var passwordsObservable =
+ this.WhenAnyValue(
+ x => x.Password,
+ x => x.ConfirmPassword,
+ (password, confirmation) =>
+ password == confirmation);
+
+ this.ValidationRule(
+ vm => vm.ConfirmPassword,
+ passwordsObservable,
+ "Passwords must match.");
+
+ SignUp = ReactiveCommand.Create(() => {}, this.IsValid());
+ }
+
+ public ReactiveCommand SignUp { get; }
+
+ public string UserName
+ {
+ get => _userName;
+ set => this.RaiseAndSetIfChanged(ref _userName, value);
+ }
+
+ public string Password
+ {
+ get => _password;
+ set => this.RaiseAndSetIfChanged(ref _password, value);
+ }
+
+ public string ConfirmPassword
+ {
+ get => _confirmPassword;
+ set => this.RaiseAndSetIfChanged(ref _confirmPassword, value);
+ }
+}
\ No newline at end of file
diff --git a/samples/Generators.Sandbox/Views/SignUpView.xaml b/samples/Generators.Sandbox/Views/SignUpView.xaml
new file mode 100644
index 0000000000..970b3a9710
--- /dev/null
+++ b/samples/Generators.Sandbox/Views/SignUpView.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/samples/Generators.Sandbox/Views/SignUpView.xaml.cs b/samples/Generators.Sandbox/Views/SignUpView.xaml.cs
new file mode 100644
index 0000000000..1f495b7dc6
--- /dev/null
+++ b/samples/Generators.Sandbox/Views/SignUpView.xaml.cs
@@ -0,0 +1,28 @@
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using Generators.Sandbox.ViewModels;
+using ReactiveUI;
+
+namespace Generators.Sandbox.Views;
+
+///
+/// This is a sample view class with typed x:Name references generated using
+/// .NET 5 source generators. The class has to be partial because x:Name
+/// references are living in a separate partial class file. See also:
+/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
+///
+public partial class SignUpView : ReactiveWindow
+{
+ public SignUpView()
+ {
+ // The InitializeComponent method is also generated automatically
+ // and lives in the autogenerated part of the partial class.
+ InitializeComponent();
+ this.WhenActivated(disposables =>
+ {
+ this.WhenAnyValue(view => view.ViewModel)
+ .BindTo(this, view => view.SignUpControl.ViewModel)
+ .DisposeWith(disposables);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
new file mode 100644
index 0000000000..b7579ed31b
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs
@@ -0,0 +1,90 @@
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Diagnostics.Controls
+{
+ internal sealed class BrushEditor : Control
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty BrushProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Brush), o => o.Brush, (o, v) => o.Brush = v);
+
+ private IBrush? _brush;
+
+ public IBrush? Brush
+ {
+ get => _brush;
+ set => SetAndRaise(BrushProperty, ref _brush, value);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == BrushProperty)
+ {
+ switch (Brush)
+ {
+ case ISolidColorBrush scb:
+ {
+ var colorView = new ColorView { Color = scb.Color };
+
+ colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
+
+ FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView });
+ ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})");
+
+ break;
+ }
+
+ default:
+
+ FlyoutBase.SetAttachedFlyout(this, null);
+ ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)");
+
+ break;
+ }
+
+ InvalidateVisual();
+ }
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ base.OnPointerPressed(e);
+
+ FlyoutBase.ShowAttachedFlyout(this);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ base.Render(context);
+
+ if (Brush != null)
+ {
+ context.FillRectangle(Brush, Bounds);
+ }
+ else
+ {
+ context.FillRectangle(Brushes.Black, Bounds);
+
+ var ft = new FormattedText("(null)",
+ CultureInfo.CurrentCulture,
+ FlowDirection.LeftToRight,
+ Typeface.Default,
+ 10,
+ Brushes.White);
+
+ context.DrawText(ft,
+ new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2));
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs
new file mode 100644
index 0000000000..7870febd0a
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs
@@ -0,0 +1,89 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.Controls
+{
+ //TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control.
+ internal sealed class CommitTextBox : TextBox, IStyleable
+ {
+ Type IStyleable.StyleKey => typeof(TextBox);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CommittedTextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v);
+
+ private string? _committedText;
+
+ public string? CommittedText
+ {
+ get => _committedText;
+ set => SetAndRaise(CommittedTextProperty, ref _committedText, value);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == CommittedTextProperty)
+ {
+ Text = CommittedText;
+ }
+ }
+
+ protected override void OnKeyUp(KeyEventArgs e)
+ {
+ base.OnKeyUp(e);
+
+ switch (e.Key)
+ {
+ case Key.Enter:
+
+ TryCommit();
+
+ e.Handled = true;
+
+ break;
+
+ case Key.Escape:
+
+ Cancel();
+
+ e.Handled = true;
+
+ break;
+ }
+ }
+
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ base.OnLostFocus(e);
+
+ TryCommit();
+ }
+
+ private void Cancel()
+ {
+ Text = CommittedText;
+ DataValidationErrors.ClearErrors(this);
+ }
+
+ private void TryCommit()
+ {
+ if (!DataValidationErrors.GetHasErrors(this))
+ {
+ CommittedText = Text;
+ }
+ else
+ {
+ Text = CommittedText;
+ DataValidationErrors.ClearErrors(this);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
index 0e412a2fa5..2412ea5325 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
@@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Priority => _priority;
public override Type AssignedType => _assignedType;
- public override string? Value
+ public override object? Value
{
- get => ConvertToString(_value);
+ get => _value;
set
{
try
{
- var convertedValue = ConvertFromString(value, Property.PropertyType);
- _target.SetValue(Property, convertedValue);
+ _target.SetValue(Property, value);
Update();
}
catch { }
@@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type? DeclaringType { get; }
public override Type PropertyType => _propertyType;
+ public override bool IsReadonly => Property.IsReadOnly;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
index 895ff41f7b..b7ee1459f7 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
@@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels
public override Type AssignedType => _assignedType;
public override Type PropertyType => _propertyType;
+ public override bool IsReadonly => !Property.CanWrite;
- public override string? Value
+ public override object? Value
{
- get => ConvertToString(_value);
+ get => _value;
set
{
try
{
- var convertedValue = ConvertFromString(value, Property.PropertyType);
- Property.SetValue(_target, convertedValue);
+ Property.SetValue(_target, value);
Update();
}
catch { }
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
index a7faf35769..aa2682e376 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
@@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class PropertyViewModel : ViewModelBase
{
- private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
- private static readonly Type[] StringParameter = { typeof(string) };
- private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
-
public abstract object Key { get; }
public abstract string Name { get; }
public abstract string Group { get; }
public abstract Type AssignedType { get; }
public abstract Type? DeclaringType { get; }
- public abstract string? Value { get; set; }
+ public abstract object? Value { get; set; }
public abstract string Priority { get; }
public abstract bool? IsAttached { get; }
public abstract void Update();
public abstract Type PropertyType { get; }
- public string Type => PropertyType == AssignedType
- ? PropertyType.GetTypeName()
- : $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
-
-
- protected static string? ConvertToString(object? value)
- {
- if (value is null)
- {
- return "(null)";
- }
-
- var converter = TypeDescriptor.GetConverter(value);
-
- //CollectionConverter does not deliver any important information. It just displays "(Collection)".
- if (!converter.CanConvertTo(typeof(string)) ||
- converter.GetType() == typeof(CollectionConverter))
- {
- return value.ToString() ?? "(null)";
- }
-
- return converter.ConvertToString(value);
- }
-
- private static object? InvokeParse(string s, Type targetType)
- {
- var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
-
- if (method != null)
- {
- return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
- }
-
- method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
-
- if (method != null)
- {
- return method.Invoke(null, new object[] { s });
- }
-
- throw new InvalidCastException("Unable to convert value.");
- }
-
- protected static object? ConvertFromString(string? s, Type targetType)
- {
- if (s is null)
- {
- return null;
- }
-
- var converter = TypeDescriptor.GetConverter(targetType);
- if (converter.CanConvertFrom(typeof(string)))
- {
- return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
- }
+ public string Type => PropertyType == AssignedType ?
+ PropertyType.GetTypeName() :
+ $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}";
- return InvokeParse(s, targetType);
- }
+ public abstract bool IsReadonly { get; }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs
new file mode 100644
index 0000000000..9425989096
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs
@@ -0,0 +1,57 @@
+using System;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Reflection;
+using Avalonia.Reactive;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal static class ReactiveExtensions
+ {
+ public static IObservable GetObservable(
+ this TOwner vm,
+ Expression> property,
+ bool fireImmediately = true)
+ where TOwner : INotifyPropertyChanged
+ {
+ return Observable.Create(o =>
+ {
+ var propertyInfo = GetPropertyInfo(property);
+
+ void Fire()
+ {
+ o.OnNext((TValue) propertyInfo.GetValue(vm)!);
+ }
+
+ void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == propertyInfo.Name)
+ {
+ Fire();
+ }
+ }
+
+ if (fireImmediately)
+ {
+ Fire();
+ }
+
+ vm.PropertyChanged += OnPropertyChanged;
+
+ return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged);
+ });
+ }
+
+ private static PropertyInfo GetPropertyInfo(this Expression> property)
+ {
+ if (property.Body is UnaryExpression unaryExpression)
+ {
+ return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member;
+ }
+
+ var memExpr = (MemberExpression)property.Body;
+
+ return (PropertyInfo)memExpr.Member;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index 2a69798c6c..97f195c91b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -60,7 +60,11 @@
DoubleTapped="PropertiesGrid_OnDoubleTapped">
-
+
+
+
+
+
("console");
_consoleSplitter = this.GetControl("consoleSplitter");
_rootGrid = this.GetControl("rootGrid");
@@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
- private void PreviewKeyDown(object? sender, KeyEventArgs e)
+ private void PreviewKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 748c2cc313..486df860c3 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -15,6 +15,7 @@
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
new file mode 100644
index 0000000000..6e7729a350
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
@@ -0,0 +1,404 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Reflection;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Diagnostics.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml.Converters;
+using Avalonia.Media;
+using Avalonia.Reactive;
+
+namespace Avalonia.Diagnostics.Views
+{
+ internal class PropertyValueEditorView : UserControl
+ {
+ private static readonly Geometry ImageIcon = Geometry.Parse(
+ "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z");
+
+ private static readonly Geometry GeometryIcon = Geometry.Parse(
+ "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z");
+
+ private static readonly ColorToBrushConverter Color2Brush = new();
+
+ private readonly CompositeDisposable _cleanup = new();
+ private PropertyViewModel? Property => (PropertyViewModel?)DataContext;
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ Content = UpdateControl();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+
+ _cleanup.Clear();
+ }
+
+ private static bool ImplementsInterface(Type type)
+ {
+ var interfaceType = typeof(TInterface);
+ return type == interfaceType || interfaceType.IsAssignableFrom(type);
+ }
+
+ private Control? UpdateControl()
+ {
+ _cleanup.Clear();
+
+ if (Property?.PropertyType is not { } propertyType)
+ return null;
+
+ TControl CreateControl(AvaloniaProperty valueProperty,
+ IValueConverter? converter = null,
+ Action? init = null,
+ AvaloniaProperty? readonlyProperty = null)
+ where TControl : Control, new()
+ {
+ var control = new TControl();
+
+ init?.Invoke(control);
+
+ control.Bind(valueProperty,
+ new Binding(nameof(Property.Value), BindingMode.TwoWay)
+ {
+ Source = Property,
+ Converter = converter ?? new ValueConverter(),
+ ConverterParameter = propertyType
+ }).DisposeWith(_cleanup);
+
+ if (readonlyProperty != null)
+ {
+ control[readonlyProperty] = Property.IsReadonly;
+ }
+ else
+ {
+ control.IsEnabled = !Property.IsReadonly;
+ }
+
+ return control;
+ }
+
+ if (propertyType == typeof(bool))
+ return CreateControl(ToggleButton.IsCheckedProperty);
+
+ //TODO: Infinity, NaN not working with NumericUpDown
+ if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double))
+ return CreateControl(
+ NumericUpDown.ValueProperty,
+ new ValueToDecimalConverter(),
+ init: n =>
+ {
+ n.Increment = 1;
+ n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 };
+ n.ParsingNumberStyle = NumberStyles.Integer;
+ },
+ readonlyProperty: NumericUpDown.IsReadOnlyProperty);
+
+ if (propertyType == typeof(Color))
+ {
+ var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center };
+
+ el.Bind(
+ Shape.FillProperty,
+ new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush })
+ .DisposeWith(_cleanup);
+
+ var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
+
+ tbl.Bind(
+ TextBlock.TextProperty,
+ new Binding(nameof(Property.Value)) { Source = Property })
+ .DisposeWith(_cleanup);
+
+ var sp = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 2,
+ Children = { el, tbl },
+ Background = Brushes.Transparent,
+ Cursor = new Cursor(StandardCursorType.Hand),
+ IsEnabled = !Property.IsReadonly
+ };
+
+ var cv = new ColorView();
+
+ cv.Bind(
+ ColorView.ColorProperty,
+ new Binding(nameof(Property.Value), BindingMode.TwoWay)
+ {
+ Source = Property, Converter = Color2Brush
+ })
+ .DisposeWith(_cleanup);
+
+ FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv });
+
+ sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp);
+
+ return sp;
+ }
+
+ if (ImplementsInterface(propertyType))
+ return CreateControl(BrushEditor.BrushProperty);
+
+ var isImage = ImplementsInterface(propertyType);
+ var isGeometry = propertyType == typeof(Geometry);
+
+ if (isImage || isGeometry)
+ {
+ var valueObservable = Property.GetObservable(x => x.Value);
+ var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
+
+ tbl.Bind(TextBlock.TextProperty,
+ valueObservable.Select(
+ value => value switch
+ {
+ IImage img => $"{img.Size.Width} x {img.Size.Height}",
+ Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}",
+ _ => "(null)"
+ }))
+ .DisposeWith(_cleanup);
+
+ var sp = new StackPanel
+ {
+ Background = Brushes.Transparent,
+ Orientation = Orientation.Horizontal,
+ Spacing = 2,
+ Children =
+ {
+ new Path
+ {
+ Data = isImage ? ImageIcon : GeometryIcon,
+ Fill = Brushes.Gray,
+ Width = 12,
+ Height = 12,
+ Stretch = Stretch.Uniform,
+ VerticalAlignment = VerticalAlignment.Center
+ },
+ tbl
+ }
+ };
+
+ if (isImage)
+ {
+ var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 };
+
+ previewImage
+ .Bind(Image.SourceProperty, valueObservable)
+ .DisposeWith(_cleanup);
+
+ ToolTip.SetTip(sp, previewImage);
+ }
+ else
+ {
+ var previewShape = new Path
+ {
+ Stretch = Stretch.Uniform,
+ Fill = Brushes.White,
+ VerticalAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Center
+ };
+
+ previewShape
+ .Bind(Path.DataProperty, valueObservable)
+ .DisposeWith(_cleanup);
+
+ ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 });
+ }
+
+ return sp;
+ }
+
+ if (propertyType.IsEnum)
+ return CreateControl(
+ SelectingItemsControl.SelectedItemProperty, init: c =>
+ {
+ c.Items = Enum.GetValues(propertyType);
+ });
+
+ var tb = CreateControl(
+ CommitTextBox.CommittedTextProperty,
+ new TextToValueConverter(),
+ t =>
+ {
+ t.Watermark = "(null)";
+ },
+ readonlyProperty: TextBox.IsReadOnlyProperty);
+
+ tb.IsReadOnly |= propertyType == typeof(object) ||
+ !StringConversionHelper.CanConvertFromString(propertyType);
+
+ if (!tb.IsReadOnly)
+ {
+ tb.GetObservable(TextBox.TextProperty).Subscribe(t =>
+ {
+ try
+ {
+ if (t != null)
+ {
+ StringConversionHelper.FromString(t, propertyType);
+ }
+
+ DataValidationErrors.ClearErrors(tb);
+ }
+ catch (Exception ex)
+ {
+ DataValidationErrors.SetError(tb, ex.GetBaseException());
+ }
+ }).DisposeWith(_cleanup);
+ }
+
+ return tb;
+ }
+
+ //HACK: ValueConverter that skips first target update
+ //TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control
+ //if the first value comes from the source or target
+ private class ValueConverter : IValueConverter
+ {
+ private bool _firstUpdate = true;
+
+ object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return Convert(value, targetType, parameter, culture);
+ }
+
+ object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (_firstUpdate)
+ {
+ _firstUpdate = false;
+
+ return BindingOperations.DoNothing;
+ }
+
+ //Note: targetType provided by Converter is simply "object"
+ return ConvertBack(value, (Type)parameter!, parameter, culture);
+ }
+
+ protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value;
+ }
+
+ protected virtual object? ConvertBack(object? value, Type targetType, object? parameter,
+ CultureInfo culture)
+ {
+ return value;
+ }
+ }
+
+ private static class StringConversionHelper
+ {
+ private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
+ private static readonly Type[] StringParameter = { typeof(string) };
+ private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
+
+ public static bool CanConvertFromString(Type type)
+ {
+ var converter = TypeDescriptor.GetConverter(type);
+
+ if (converter.CanConvertFrom(typeof(string)))
+ return true;
+
+ return GetParseMethod(type, out _) != null;
+ }
+
+ public static string? ToString(object o)
+ {
+ var converter = TypeDescriptor.GetConverter(o);
+
+ //CollectionConverter does not deliver any important information. It just displays "(Collection)".
+ if (!converter.CanConvertTo(typeof(string)) ||
+ converter.GetType() == typeof(CollectionConverter))
+ return o.ToString();
+
+ return converter.ConvertToInvariantString(o);
+ }
+
+ public static object? FromString(string str, Type type)
+ {
+ var converter = TypeDescriptor.GetConverter(type);
+
+ return converter.CanConvertFrom(typeof(string)) ?
+ converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) :
+ InvokeParse(str, type);
+ }
+
+ private static object? InvokeParse(string s, Type targetType)
+ {
+ var m = GetParseMethod(targetType, out var hasFormat);
+
+ if (m == null)
+ throw new InvalidOperationException();
+
+ return m.Invoke(null,
+ hasFormat ?
+ new object[] { s, CultureInfo.InvariantCulture } :
+ new object[] { s });
+ }
+
+ private static MethodInfo? GetParseMethod(Type type, out bool hasFormat)
+ {
+ var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null);
+
+ if (m != null)
+ {
+ hasFormat = true;
+
+ return m;
+ }
+
+ hasFormat = false;
+
+ return type.GetMethod("Parse", PublicStatic, null, StringParameter, null);
+ }
+ }
+
+ private sealed class ValueToDecimalConverter : ValueConverter
+ {
+ protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return System.Convert.ToDecimal(value);
+ }
+
+ protected override object? ConvertBack(object? value, Type targetType, object? parameter,
+ CultureInfo culture)
+ {
+ return System.Convert.ChangeType(value, targetType);
+ }
+ }
+
+ private sealed class TextToValueConverter : ValueConverter
+ {
+ protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value is null ? null : StringConversionHelper.ToString(value);
+ }
+
+ protected override object? ConvertBack(object? value, Type targetType, object? parameter,
+ CultureInfo culture)
+ {
+ if (value is not string s)
+ return null;
+
+ try
+ {
+ return StringConversionHelper.FromString(s, targetType);
+ }
+ catch
+ {
+ return BindingOperations.DoNothing;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index 8428e3aae7..0c7095f2f5 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
@@ -92,6 +92,7 @@
+
diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.csproj b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj
new file mode 100644
index 0000000000..c6e32a3a4f
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Avalonia.Generators.csproj
@@ -0,0 +1,32 @@
+
+
+ netstandard2.0
+ false
+ Avalonia.Generators
+ $(DefineConstants);XAMLX_INTERNAL
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.props b/src/tools/Avalonia.Generators/Avalonia.Generators.props
new file mode 100644
index 0000000000..08cbeff1ba
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Avalonia.Generators.props
@@ -0,0 +1,22 @@
+
+
+ true
+ InitializeComponent
+ internal
+ *
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
new file mode 100644
index 0000000000..4b426172f8
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/Domain/ICodeGenerator.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Generators.Common.Domain;
+
+internal interface ICodeGenerator
+{
+ string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable names);
+}
diff --git a/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
new file mode 100644
index 0000000000..04dbf9cbb9
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/Domain/IGlobPattern.cs
@@ -0,0 +1,6 @@
+namespace Avalonia.Generators.Common.Domain;
+
+internal interface IGlobPattern
+{
+ bool Matches(string str);
+}
diff --git a/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
new file mode 100644
index 0000000000..cb5488d8a3
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/Domain/INameResolver.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using XamlX.Ast;
+
+namespace Avalonia.Generators.Common.Domain;
+
+internal enum NamedFieldModifier
+{
+ Public = 0,
+ Private = 1,
+ Internal = 2,
+ Protected = 3,
+}
+
+internal interface INameResolver
+{
+ IReadOnlyList ResolveNames(XamlDocument xaml);
+}
+
+internal record ResolvedName(string TypeName, string Name, string FieldModifier);
diff --git a/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
new file mode 100644
index 0000000000..c3c219e3f0
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/Domain/IViewResolver.cs
@@ -0,0 +1,11 @@
+using XamlX.Ast;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Generators.Common.Domain;
+
+internal interface IViewResolver
+{
+ ResolvedView ResolveView(string xaml);
+}
+
+internal record ResolvedView(string ClassName, IXamlType XamlType, string Namespace, XamlDocument Xaml);
diff --git a/src/tools/Avalonia.Generators/Common/GlobPattern.cs b/src/tools/Avalonia.Generators/Common/GlobPattern.cs
new file mode 100644
index 0000000000..484e17d787
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/GlobPattern.cs
@@ -0,0 +1,18 @@
+using System.Text.RegularExpressions;
+using Avalonia.Generators.Common.Domain;
+
+namespace Avalonia.Generators.Common;
+
+internal class GlobPattern : IGlobPattern
+{
+ private const RegexOptions GlobOptions = RegexOptions.IgnoreCase | RegexOptions.Singleline;
+ private readonly Regex _regex;
+
+ public GlobPattern(string pattern)
+ {
+ var expression = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
+ _regex = new Regex(expression, GlobOptions);
+ }
+
+ public bool Matches(string str) => _regex.IsMatch(str);
+}
diff --git a/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
new file mode 100644
index 0000000000..1358ee7920
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/GlobPatternGroup.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Generators.Common.Domain;
+
+namespace Avalonia.Generators.Common;
+
+internal class GlobPatternGroup : IGlobPattern
+{
+ private readonly GlobPattern[] _patterns;
+
+ public GlobPatternGroup(IEnumerable patterns) =>
+ _patterns = patterns
+ .Select(pattern => new GlobPattern(pattern))
+ .ToArray();
+
+ public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str));
+}
diff --git a/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
new file mode 100644
index 0000000000..04352298c8
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/ResolverExtensions.cs
@@ -0,0 +1,25 @@
+using System.Linq;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Generators.Common;
+
+internal static class ResolverExtensions
+{
+ public static bool IsAvaloniaStyledElement(this IXamlType clrType) =>
+ clrType.HasStyledElementBaseType() ||
+ clrType.HasIStyledElementInterface();
+
+ private static bool HasStyledElementBaseType(this IXamlType clrType)
+ {
+ // Check for the base type since IStyledElement interface is removed.
+ // https://github.com/AvaloniaUI/Avalonia/pull/9553
+ if (clrType.FullName == "Avalonia.StyledElement")
+ return true;
+ return clrType.BaseType != null && IsAvaloniaStyledElement(clrType.BaseType);
+ }
+
+ private static bool HasIStyledElementInterface(this IXamlType clrType) =>
+ clrType.Interfaces.Any(abstraction =>
+ abstraction.IsInterface &&
+ abstraction.FullName == "Avalonia.IStyledElement");
+}
diff --git a/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
new file mode 100644
index 0000000000..7ed19eb84c
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Avalonia.Generators.Common.Domain;
+using XamlX;
+using XamlX.Ast;
+
+namespace Avalonia.Generators.Common;
+
+internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
+{
+ private readonly List _items = new();
+ private readonly string _defaultFieldModifier;
+
+ public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal)
+ {
+ _defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant();
+ }
+
+ public IReadOnlyList ResolveNames(XamlDocument xaml)
+ {
+ _items.Clear();
+ xaml.Root.Visit(this);
+ xaml.Root.VisitChildren(this);
+ return _items;
+ }
+
+ IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
+ {
+ if (node is not XamlAstObjectNode objectNode)
+ return node;
+
+ var clrType = objectNode.Type.GetClrType();
+ if (!clrType.IsAvaloniaStyledElement())
+ return node;
+
+ foreach (var child in objectNode.Children)
+ {
+ if (child is XamlAstXamlPropertyValueNode propertyValueNode &&
+ propertyValueNode.Property is XamlAstNamePropertyReference namedProperty &&
+ namedProperty.Name == "Name" &&
+ propertyValueNode.Values.Count > 0 &&
+ propertyValueNode.Values[0] is XamlAstTextNode text)
+ {
+ var fieldModifier = TryGetFieldModifier(objectNode);
+ var typeName = $@"{clrType.Namespace}.{clrType.Name}";
+ var typeAgs = clrType.GenericArguments.Select(arg => arg.FullName).ToImmutableList();
+ var genericTypeName = typeAgs.Count == 0
+ ? $"global::{typeName}"
+ : $@"global::{typeName}<{string.Join(", ", typeAgs.Select(arg => $"global::{arg}"))}>";
+
+ var resolvedName = new ResolvedName(genericTypeName, text.Text, fieldModifier);
+ if (_items.Contains(resolvedName))
+ continue;
+ _items.Add(resolvedName);
+ }
+ }
+
+ return node;
+ }
+
+ void IXamlAstVisitor.Push(IXamlAstNode node) { }
+
+ void IXamlAstVisitor.Pop() { }
+
+ private string TryGetFieldModifier(XamlAstObjectNode objectNode)
+ {
+ // We follow Xamarin.Forms API behavior in terms of x:FieldModifier here:
+ // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/xaml/field-modifiers
+ // However, by default we use 'internal' field modifier here for generated
+ // x:Name references for historical purposes and WPF compatibility.
+ //
+ var fieldModifierType = objectNode
+ .Children
+ .OfType()
+ .Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006)
+ .Select(dir => dir.Values[0])
+ .OfType()
+ .Select(txt => txt.Text)
+ .FirstOrDefault();
+
+ return fieldModifierType?.ToLowerInvariant() switch
+ {
+ "private" => "private",
+ "public" => "public",
+ "protected" => "protected",
+ "internal" => "internal",
+ "notpublic" => "internal",
+ _ => _defaultFieldModifier
+ };
+ }
+}
diff --git a/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
new file mode 100644
index 0000000000..5bbe0c060d
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Common/XamlXViewResolver.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Generators.Common.Domain;
+using Avalonia.Generators.Compiler;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Parsers;
+
+namespace Avalonia.Generators.Common;
+
+internal class XamlXViewResolver : IViewResolver, IXamlAstVisitor
+{
+ private readonly RoslynTypeSystem _typeSystem;
+ private readonly MiniCompiler _compiler;
+ private readonly bool _checkTypeValidity;
+ private readonly Action _onTypeInvalid;
+ private readonly Action _onUnhandledError;
+
+ private ResolvedView _resolvedClass;
+ private XamlDocument _xaml;
+
+ public XamlXViewResolver(
+ RoslynTypeSystem typeSystem,
+ MiniCompiler compiler,
+ bool checkTypeValidity = false,
+ Action onTypeInvalid = null,
+ Action onUnhandledError = null)
+ {
+ _checkTypeValidity = checkTypeValidity;
+ _onTypeInvalid = onTypeInvalid;
+ _onUnhandledError = onUnhandledError;
+ _typeSystem = typeSystem;
+ _compiler = compiler;
+ }
+
+ public ResolvedView ResolveView(string xaml)
+ {
+ try
+ {
+ _resolvedClass = null;
+ _xaml = XDocumentXamlParser.Parse(xaml, new Dictionary
+ {
+ {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
+ });
+
+ _compiler.Transform(_xaml);
+ _xaml.Root.Visit(this);
+ _xaml.Root.VisitChildren(this);
+ return _resolvedClass;
+ }
+ catch (Exception exception)
+ {
+ _onUnhandledError?.Invoke(exception);
+ return null;
+ }
+ }
+
+ IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
+ {
+ if (node is not XamlAstObjectNode objectNode)
+ return node;
+
+ var clrType = objectNode.Type.GetClrType();
+ if (!clrType.IsAvaloniaStyledElement())
+ return node;
+
+ foreach (var child in objectNode.Children)
+ {
+ if (child is XamlAstXmlDirective directive &&
+ directive.Name == "Class" &&
+ directive.Namespace == XamlNamespaces.Xaml2006 &&
+ directive.Values[0] is XamlAstTextNode text)
+ {
+ if (_checkTypeValidity)
+ {
+ var existingType = _typeSystem.FindType(text.Text);
+ if (existingType == null)
+ {
+ _onTypeInvalid?.Invoke(text.Text);
+ return node;
+ }
+ }
+
+ var split = text.Text.Split('.');
+ var nameSpace = string.Join(".", split.Take(split.Length - 1));
+ var className = split.Last();
+
+ _resolvedClass = new ResolvedView(className, clrType, nameSpace, _xaml);
+ return node;
+ }
+ }
+
+ return node;
+ }
+
+ void IXamlAstVisitor.Push(IXamlAstNode node) { }
+
+ void IXamlAstVisitor.Pop() { }
+}
diff --git a/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs b/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs
new file mode 100644
index 0000000000..e7c60c79ad
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Compiler/DataTemplateTransformer.cs
@@ -0,0 +1,17 @@
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Generators.Compiler;
+
+internal class DataTemplateTransformer : IXamlAstTransformer
+{
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstObjectNode objectNode &&
+ objectNode.Type is XamlAstXmlTypeReference typeReference &&
+ (typeReference.Name == "DataTemplate" ||
+ typeReference.Name == "ControlTemplate"))
+ objectNode.Children.Clear();
+ return node;
+ }
+}
\ No newline at end of file
diff --git a/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
new file mode 100644
index 0000000000..71f34d173c
--- /dev/null
+++ b/src/tools/Avalonia.Generators/Compiler/MiniCompiler.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using XamlX.Compiler;
+using XamlX.Emit;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Generators.Compiler;
+
+internal sealed class MiniCompiler : XamlCompiler