committed by
GitHub
107 changed files with 3634 additions and 144 deletions
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,7 @@ |
|||
<Application xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Generators.Sandbox.App"> |
|||
<Application.Styles> |
|||
<FluentTheme /> |
|||
</Application.Styles> |
|||
</Application> |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Generators.Sandbox.Controls" |
|||
x:Class="Generators.Sandbox.Controls.SignUpView"> |
|||
<StackPanel> |
|||
<controls:CustomTextBox Margin="0 10 0 0" |
|||
x:Name="UserNameTextBox" |
|||
Watermark="Please, enter user name..." |
|||
UseFloatingWatermark="True" /> |
|||
<TextBlock x:Name="UserNameValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="PasswordTextBox" |
|||
Watermark="Please, enter your password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="PasswordValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="ConfirmPasswordTextBox" |
|||
Watermark="Please, confirm the password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="ConfirmPasswordValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBlock> |
|||
<TextBlock.Inlines> |
|||
<InlineCollection> |
|||
<Run x:Name="SignUpButtonDescription" /> |
|||
</InlineCollection> |
|||
</TextBlock.Inlines> |
|||
</TextBlock> |
|||
<Button Margin="0 10 0 5" |
|||
Content="Sign up" |
|||
x:Name="SignUpButton" /> |
|||
<TextBlock x:Name="CompoundValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// 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/
|
|||
/// </summary>
|
|||
public partial class SignUpView : ReactiveUserControl<SignUpViewModel> |
|||
{ |
|||
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."; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<AvaloniaResource Include="**\*.xaml"/> |
|||
<!-- Note this AdditionalFiles directive. --> |
|||
<AdditionalFiles Include="**\*.xaml"/> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/> |
|||
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\BuildTargets.targets"/> |
|||
<Import Project="..\..\build\SourceGenerators.props"/> |
|||
</Project> |
|||
@ -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<App>() |
|||
.UseReactiveUI() |
|||
.UsePlatformDetect() |
|||
.LogToTrace(); |
|||
} |
|||
@ -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<Unit, Unit> 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); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Generators.Sandbox.Controls" |
|||
x:Class="Generators.Sandbox.Views.SignUpView"> |
|||
<StackPanel Margin="10"> |
|||
<TextBlock Text="Sign Up" /> |
|||
<controls:SignUpView x:Name="SignUpControl" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,28 @@ |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.ReactiveUI; |
|||
using Generators.Sandbox.ViewModels; |
|||
using ReactiveUI; |
|||
|
|||
namespace Generators.Sandbox.Views; |
|||
|
|||
/// <summary>
|
|||
/// 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/
|
|||
/// </summary>
|
|||
public partial class SignUpView : ReactiveWindow<SignUpViewModel> |
|||
{ |
|||
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); |
|||
}); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Brush" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty = |
|||
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>( |
|||
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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommittedText" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<CommitTextBox, string?> CommittedTextProperty = |
|||
AvaloniaProperty.RegisterDirect<CommitTextBox, string?>( |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<TValue> GetObservable<TOwner, TValue>( |
|||
this TOwner vm, |
|||
Expression<Func<TOwner, TValue>> property, |
|||
bool fireImmediately = true) |
|||
where TOwner : INotifyPropertyChanged |
|||
{ |
|||
return Observable.Create<TValue>(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<TOwner, TValue>(this Expression<Func<TOwner, TValue>> property) |
|||
{ |
|||
if (property.Body is UnaryExpression unaryExpression) |
|||
{ |
|||
return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; |
|||
} |
|||
|
|||
var memExpr = (MemberExpression)property.Body; |
|||
|
|||
return (PropertyInfo)memExpr.Member; |
|||
} |
|||
} |
|||
} |
|||
@ -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<TInterface>(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<TControl>(AvaloniaProperty valueProperty, |
|||
IValueConverter? converter = null, |
|||
Action<TControl>? 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<CheckBox>(ToggleButton.IsCheckedProperty); |
|||
|
|||
//TODO: Infinity, NaN not working with NumericUpDown
|
|||
if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) |
|||
return CreateControl<NumericUpDown>( |
|||
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<IBrush>(propertyType)) |
|||
return CreateControl<BrushEditor>(BrushEditor.BrushProperty); |
|||
|
|||
var isImage = ImplementsInterface<IImage>(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<ComboBox>( |
|||
SelectingItemsControl.SelectedItemProperty, init: c => |
|||
{ |
|||
c.Items = Enum.GetValues(propertyType); |
|||
}); |
|||
|
|||
var tb = CreateControl<CommitTextBox>( |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<IncludeBuildOutput>false</IncludeBuildOutput> |
|||
<PackageId>Avalonia.Generators</PackageId> |
|||
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants> |
|||
<IsPackable>true</IsPackable> |
|||
<IsRoslynComponent>true</IsRoslynComponent> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" /> |
|||
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" /> |
|||
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Include="Avalonia.Generators.props" Pack="true" PackagePath="buildTransitive/$(PackageId).props" /> |
|||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Label="InternalsVisibleTo"> |
|||
<InternalsVisibleTo Include="Avalonia.Generators.Tests, PublicKey=$(AvaloniaPublicKey)" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
|||
</Project> |
|||
@ -0,0 +1,22 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<PropertyGroup> |
|||
<AvaloniaNameGeneratorIsEnabled Condition="'$(AvaloniaNameGeneratorIsEnabled)' == ''">true</AvaloniaNameGeneratorIsEnabled> |
|||
<AvaloniaNameGeneratorBehavior Condition="'$(AvaloniaNameGeneratorBehavior)' == ''">InitializeComponent</AvaloniaNameGeneratorBehavior> |
|||
<AvaloniaNameGeneratorDefaultFieldModifier Condition="'$(AvaloniaNameGeneratorDefaultFieldModifier)' == ''">internal</AvaloniaNameGeneratorDefaultFieldModifier> |
|||
<AvaloniaNameGeneratorFilterByPath Condition="'$(AvaloniaNameGeneratorFilterByPath)' == ''">*</AvaloniaNameGeneratorFilterByPath> |
|||
<AvaloniaNameGeneratorFilterByNamespace Condition="'$(AvaloniaNameGeneratorFilterByNamespace)' == ''">*</AvaloniaNameGeneratorFilterByNamespace> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="SourceItemGroup"/> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorIsEnabled" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorBehavior" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorDefaultFieldModifier" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByPath" /> |
|||
<CompilerVisibleProperty Include="AvaloniaNameGeneratorFilterByNamespace" /> |
|||
</ItemGroup> |
|||
<Target Name="_InjectAdditionalFiles" BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun"> |
|||
<ItemGroup> |
|||
<AdditionalFiles Include="@(AvaloniaXaml)" SourceItemGroup="AvaloniaXaml" /> |
|||
</ItemGroup> |
|||
</Target> |
|||
</Project> |
|||
@ -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<ResolvedName> names); |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Avalonia.Generators.Common.Domain; |
|||
|
|||
internal interface IGlobPattern |
|||
{ |
|||
bool Matches(string str); |
|||
} |
|||
@ -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<ResolvedName> ResolveNames(XamlDocument xaml); |
|||
} |
|||
|
|||
internal record ResolvedName(string TypeName, string Name, string FieldModifier); |
|||
@ -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); |
|||
@ -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); |
|||
} |
|||
@ -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<string> patterns) => |
|||
_patterns = patterns |
|||
.Select(pattern => new GlobPattern(pattern)) |
|||
.ToArray(); |
|||
|
|||
public bool Matches(string str) => _patterns.Any(pattern => pattern.Matches(str)); |
|||
} |
|||
@ -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"); |
|||
} |
|||
@ -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<ResolvedName> _items = new(); |
|||
private readonly string _defaultFieldModifier; |
|||
|
|||
public XamlXNameResolver(NamedFieldModifier namedFieldModifier = NamedFieldModifier.Internal) |
|||
{ |
|||
_defaultFieldModifier = namedFieldModifier.ToString().ToLowerInvariant(); |
|||
} |
|||
|
|||
public IReadOnlyList<ResolvedName> 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<XamlAstXmlDirective>() |
|||
.Where(dir => dir.Name == "FieldModifier" && dir.Namespace == XamlNamespaces.Xaml2006) |
|||
.Select(dir => dir.Values[0]) |
|||
.OfType<XamlAstTextNode>() |
|||
.Select(txt => txt.Text) |
|||
.FirstOrDefault(); |
|||
|
|||
return fieldModifierType?.ToLowerInvariant() switch |
|||
{ |
|||
"private" => "private", |
|||
"public" => "public", |
|||
"protected" => "protected", |
|||
"internal" => "internal", |
|||
"notpublic" => "internal", |
|||
_ => _defaultFieldModifier |
|||
}; |
|||
} |
|||
} |
|||
@ -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<string> _onTypeInvalid; |
|||
private readonly Action<Exception> _onUnhandledError; |
|||
|
|||
private ResolvedView _resolvedClass; |
|||
private XamlDocument _xaml; |
|||
|
|||
public XamlXViewResolver( |
|||
RoslynTypeSystem typeSystem, |
|||
MiniCompiler compiler, |
|||
bool checkTypeValidity = false, |
|||
Action<string> onTypeInvalid = null, |
|||
Action<Exception> 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<string, string> |
|||
{ |
|||
{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() { } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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<object, IXamlEmitResult> |
|||
{ |
|||
public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
|||
|
|||
public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) |
|||
{ |
|||
var mappings = new XamlLanguageTypeMappings(typeSystem); |
|||
foreach (var additionalType in additionalTypes) |
|||
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType)); |
|||
|
|||
var configuration = new TransformerConfiguration( |
|||
typeSystem, |
|||
typeSystem.Assemblies.First(), |
|||
mappings); |
|||
return new MiniCompiler(configuration); |
|||
} |
|||
|
|||
private MiniCompiler(TransformerConfiguration configuration) |
|||
: base(configuration, new XamlLanguageEmitMappings<object, IXamlEmitResult>(), false) |
|||
{ |
|||
Transformers.Add(new NameDirectiveTransformer()); |
|||
Transformers.Add(new DataTemplateTransformer()); |
|||
Transformers.Add(new KnownDirectivesTransformer()); |
|||
Transformers.Add(new XamlIntrinsicsTransformer()); |
|||
Transformers.Add(new XArgumentsTransformer()); |
|||
Transformers.Add(new TypeReferenceResolver()); |
|||
} |
|||
|
|||
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen( |
|||
IFileSource file, |
|||
Func<string, IXamlType, |
|||
IXamlTypeBuilder<object>> createSubType, |
|||
Func<string, IXamlType, IEnumerable<IXamlType>, |
|||
IXamlTypeBuilder<object>> createDelegateType, |
|||
object codeGen, |
|||
XamlRuntimeContext<object, IXamlEmitResult> context, |
|||
bool needContextLocal) => |
|||
throw new NotSupportedException(); |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal class NameDirectiveTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
for (var index = 0; index < objectNode.Children.Count; index++) |
|||
{ |
|||
var child = objectNode.Children[index]; |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Name == "Name") |
|||
objectNode.Children[index] = new XamlAstXamlPropertyValueNode( |
|||
directive, |
|||
new XamlAstNamePropertyReference(directive, objectNode.Type, "Name", objectNode.Type), |
|||
directive.Values); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.Compiler; |
|||
|
|||
internal class RoslynTypeSystem : IXamlTypeSystem |
|||
{ |
|||
private readonly List<IXamlAssembly> _assemblies = new(); |
|||
|
|||
public RoslynTypeSystem(CSharpCompilation compilation) |
|||
{ |
|||
_assemblies.Add(new RoslynAssembly(compilation.Assembly)); |
|||
|
|||
var assemblySymbols = compilation |
|||
.References |
|||
.Select(compilation.GetAssemblyOrModuleSymbol) |
|||
.OfType<IAssemblySymbol>() |
|||
.Select(assembly => new RoslynAssembly(assembly)) |
|||
.ToList(); |
|||
|
|||
_assemblies.AddRange(assemblySymbols); |
|||
} |
|||
|
|||
public IEnumerable<IXamlAssembly> Assemblies => _assemblies; |
|||
|
|||
public IXamlAssembly FindAssembly(string name) => |
|||
Assemblies |
|||
.FirstOrDefault(a => string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase)); |
|||
|
|||
public IXamlType FindType(string name) => |
|||
_assemblies |
|||
.Select(assembly => assembly.FindType(name)) |
|||
.FirstOrDefault(type => type != null); |
|||
|
|||
public IXamlType FindType(string name, string assembly) => |
|||
_assemblies |
|||
.Select(assemblyInstance => assemblyInstance.FindType(name)) |
|||
.FirstOrDefault(type => type != null); |
|||
} |
|||
|
|||
internal class RoslynAssembly : IXamlAssembly |
|||
{ |
|||
private readonly IAssemblySymbol _symbol; |
|||
|
|||
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol; |
|||
|
|||
public bool Equals(IXamlAssembly other) => |
|||
other is RoslynAssembly roslynAssembly && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => |
|||
_symbol.GetAttributes() |
|||
.Select(data => new RoslynAttribute(data, this)) |
|||
.ToList(); |
|||
|
|||
public IXamlType FindType(string fullName) |
|||
{ |
|||
var type = _symbol.GetTypeByMetadataName(fullName); |
|||
return type is null ? null : new RoslynType(type, this); |
|||
} |
|||
} |
|||
|
|||
internal class RoslynAttribute : IXamlCustomAttribute |
|||
{ |
|||
private readonly AttributeData _data; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynAttribute(AttributeData data, RoslynAssembly assembly) |
|||
{ |
|||
_data = data; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlCustomAttribute other) => |
|||
other is RoslynAttribute attribute && |
|||
_data == attribute._data; |
|||
|
|||
public IXamlType Type => new RoslynType(_data.AttributeClass, _assembly); |
|||
|
|||
public List<object> Parameters => |
|||
_data.ConstructorArguments |
|||
.Select(argument => argument.Value) |
|||
.ToList(); |
|||
|
|||
public Dictionary<string, object> Properties => |
|||
_data.NamedArguments.ToDictionary( |
|||
pair => pair.Key, |
|||
pair => pair.Value.Value); |
|||
} |
|||
|
|||
internal class RoslynType : IXamlType |
|||
{ |
|||
private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( |
|||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, |
|||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | |
|||
SymbolDisplayGenericsOptions.IncludeTypeConstraints | |
|||
SymbolDisplayGenericsOptions.IncludeVariance); |
|||
|
|||
private readonly RoslynAssembly _assembly; |
|||
private readonly INamedTypeSymbol _symbol; |
|||
|
|||
public RoslynType(INamedTypeSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlType other) => |
|||
other is RoslynType roslynType && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynType._symbol); |
|||
|
|||
public object Id => _symbol; |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public string Namespace => _symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); |
|||
|
|||
public string FullName => $"{Namespace}.{Name}"; |
|||
|
|||
public IXamlAssembly Assembly => _assembly; |
|||
|
|||
public IReadOnlyList<IXamlProperty> Properties => |
|||
_symbol.GetMembers() |
|||
.Where(member => member.Kind == SymbolKind.Property) |
|||
.OfType<IPropertySymbol>() |
|||
.Select(property => new RoslynProperty(property, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IReadOnlyList<IXamlEventInfo> Events { get; } = new List<IXamlEventInfo>(); |
|||
|
|||
public IReadOnlyList<IXamlField> Fields { get; } = new List<IXamlField>(); |
|||
|
|||
public IReadOnlyList<IXamlMethod> Methods { get; } = new List<IXamlMethod>(); |
|||
|
|||
public IReadOnlyList<IXamlConstructor> Constructors => |
|||
_symbol.Constructors |
|||
.Select(method => new RoslynConstructor(method, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
|
|||
public IReadOnlyList<IXamlType> GenericArguments { get; private set; } = new List<IXamlType>(); |
|||
|
|||
public bool IsAssignableFrom(IXamlType type) => type == this; |
|||
|
|||
public IXamlType MakeGenericType(IReadOnlyList<IXamlType> typeArguments) |
|||
{ |
|||
GenericArguments = typeArguments; |
|||
return this; |
|||
} |
|||
|
|||
public IXamlType GenericTypeDefinition => this; |
|||
|
|||
public bool IsArray => false; |
|||
|
|||
public IXamlType ArrayElementType { get; } = null; |
|||
|
|||
public IXamlType MakeArrayType(int dimensions) => null; |
|||
|
|||
public IXamlType BaseType => _symbol.BaseType == null ? null : new RoslynType(_symbol.BaseType, _assembly); |
|||
|
|||
public bool IsValueType { get; } = false; |
|||
|
|||
public bool IsEnum { get; } = false; |
|||
|
|||
public IReadOnlyList<IXamlType> Interfaces => |
|||
_symbol.AllInterfaces |
|||
.Select(abstraction => new RoslynType(abstraction, _assembly)) |
|||
.ToList(); |
|||
|
|||
public bool IsInterface => _symbol.IsAbstract; |
|||
|
|||
public IXamlType GetEnumUnderlyingType() => null; |
|||
|
|||
public IReadOnlyList<IXamlType> GenericParameters { get; } = new List<IXamlType>(); |
|||
} |
|||
|
|||
internal class RoslynConstructor : IXamlConstructor |
|||
{ |
|||
private readonly IMethodSymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynConstructor(IMethodSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlConstructor other) => |
|||
other is RoslynConstructor roslynConstructor && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynConstructor._symbol); |
|||
|
|||
public bool IsPublic => true; |
|||
|
|||
public bool IsStatic => false; |
|||
|
|||
public IReadOnlyList<IXamlType> Parameters => |
|||
_symbol.Parameters |
|||
.Select(parameter => parameter.Type) |
|||
.OfType<INamedTypeSymbol>() |
|||
.Select(type => new RoslynType(type, _assembly)) |
|||
.ToList(); |
|||
} |
|||
|
|||
internal class RoslynProperty : IXamlProperty |
|||
{ |
|||
private readonly IPropertySymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynProperty(IPropertySymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlProperty other) => |
|||
other is RoslynProperty roslynProperty && |
|||
SymbolEqualityComparer.Default.Equals(_symbol, roslynProperty._symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public IXamlType PropertyType => |
|||
_symbol.Type is INamedTypeSymbol namedTypeSymbol |
|||
? new RoslynType(namedTypeSymbol, _assembly) |
|||
: null; |
|||
|
|||
public IXamlMethod Getter => _symbol.GetMethod == null ? null : new RoslynMethod(_symbol.GetMethod, _assembly); |
|||
|
|||
public IXamlMethod Setter => _symbol.SetMethod == null ? null : new RoslynMethod(_symbol.SetMethod, _assembly); |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
|
|||
public IReadOnlyList<IXamlType> IndexerParameters { get; } = new List<IXamlType>(); |
|||
} |
|||
|
|||
internal class RoslynMethod : IXamlMethod |
|||
{ |
|||
private readonly IMethodSymbol _symbol; |
|||
private readonly RoslynAssembly _assembly; |
|||
|
|||
public RoslynMethod(IMethodSymbol symbol, RoslynAssembly assembly) |
|||
{ |
|||
_symbol = symbol; |
|||
_assembly = assembly; |
|||
} |
|||
|
|||
public bool Equals(IXamlMethod other) => |
|||
other is RoslynMethod roslynMethod && |
|||
SymbolEqualityComparer.Default.Equals(roslynMethod._symbol, _symbol); |
|||
|
|||
public string Name => _symbol.Name; |
|||
|
|||
public bool IsPublic => true; |
|||
|
|||
public bool IsStatic => false; |
|||
|
|||
public IXamlType ReturnType => new RoslynType((INamedTypeSymbol) _symbol.ReturnType, _assembly); |
|||
|
|||
public IReadOnlyList<IXamlType> Parameters => |
|||
_symbol.Parameters.Select(parameter => parameter.Type) |
|||
.OfType<INamedTypeSymbol>() |
|||
.Select(type => new RoslynType(type, _assembly)) |
|||
.ToList(); |
|||
|
|||
public IXamlType DeclaringType => new RoslynType((INamedTypeSymbol)_symbol.ReceiverType, _assembly); |
|||
|
|||
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => null; |
|||
|
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes { get; } = new List<IXamlCustomAttribute>(); |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators; |
|||
|
|||
internal static class GeneratorContextExtensions |
|||
{ |
|||
private const string UnhandledErrorDescriptorId = "AXN0002"; |
|||
private const string InvalidTypeDescriptorId = "AXN0001"; |
|||
|
|||
public static string GetMsBuildProperty( |
|||
this GeneratorExecutionContext context, |
|||
string name, |
|||
string defaultValue = "") |
|||
{ |
|||
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); |
|||
return value ?? defaultValue; |
|||
} |
|||
|
|||
public static void ReportNameGeneratorUnhandledError(this GeneratorExecutionContext context, Exception error) => |
|||
context.Report(UnhandledErrorDescriptorId, |
|||
"Unhandled exception occured while generating typed Name references. " + |
|||
"Please file an issue: https://github.com/avaloniaui/Avalonia.Generators", |
|||
error.ToString()); |
|||
|
|||
public static void ReportNameGeneratorInvalidType(this GeneratorExecutionContext context, string typeName) => |
|||
context.Report(InvalidTypeDescriptorId, |
|||
$"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + |
|||
$"The type '{typeName}' does not exist in the assembly."); |
|||
|
|||
private static void Report(this GeneratorExecutionContext context, string id, string title, string message = null) => |
|||
context.ReportDiagnostic( |
|||
Diagnostic.Create( |
|||
new DiagnosticDescriptor(id, title, message ?? title, "Usage", DiagnosticSeverity.Error, true), |
|||
Location.None)); |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using System; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Avalonia.Generators.NameGenerator; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators; |
|||
|
|||
// When update these enum values, don't forget to update Avalonia.Generators.props.
|
|||
internal enum BuildProperties |
|||
{ |
|||
AvaloniaNameGeneratorIsEnabled = 0, |
|||
AvaloniaNameGeneratorBehavior = 1, |
|||
AvaloniaNameGeneratorDefaultFieldModifier = 2, |
|||
AvaloniaNameGeneratorFilterByPath = 3, |
|||
AvaloniaNameGeneratorFilterByNamespace = 4, |
|||
AvaloniaNameGeneratorViewFileNamingStrategy = 5, |
|||
|
|||
// TODO add other generators properties here.
|
|||
} |
|||
|
|||
internal class GeneratorOptions |
|||
{ |
|||
private readonly GeneratorExecutionContext _context; |
|||
|
|||
public GeneratorOptions(GeneratorExecutionContext context) => _context = context; |
|||
|
|||
public bool AvaloniaNameGeneratorIsEnabled => GetBoolProperty( |
|||
BuildProperties.AvaloniaNameGeneratorIsEnabled, |
|||
true); |
|||
|
|||
public Behavior AvaloniaNameGeneratorBehavior => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorBehavior, |
|||
Behavior.InitializeComponent); |
|||
|
|||
public NamedFieldModifier AvaloniaNameGeneratorClassFieldModifier => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorDefaultFieldModifier, |
|||
NamedFieldModifier.Internal); |
|||
|
|||
public ViewFileNamingStrategy AvaloniaNameGeneratorViewFileNamingStrategy => GetEnumProperty( |
|||
BuildProperties.AvaloniaNameGeneratorViewFileNamingStrategy, |
|||
ViewFileNamingStrategy.NamespaceAndClassName); |
|||
|
|||
public string[] AvaloniaNameGeneratorFilterByPath => GetStringArrayProperty( |
|||
BuildProperties.AvaloniaNameGeneratorFilterByPath, |
|||
"*"); |
|||
|
|||
public string[] AvaloniaNameGeneratorFilterByNamespace => GetStringArrayProperty( |
|||
BuildProperties.AvaloniaNameGeneratorFilterByNamespace, |
|||
"*"); |
|||
|
|||
private string[] GetStringArrayProperty(BuildProperties name, string defaultValue) |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue); |
|||
return value.Contains(";") ? value.Split(';') : new[] {value}; |
|||
} |
|||
|
|||
private TEnum GetEnumProperty<TEnum>(BuildProperties name, TEnum defaultValue) where TEnum : struct |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
|||
return Enum.TryParse(value, true, out TEnum behavior) ? behavior : defaultValue; |
|||
} |
|||
|
|||
private bool GetBoolProperty(BuildProperties name, bool defaultValue) |
|||
{ |
|||
var key = name.ToString(); |
|||
var value = _context.GetMsBuildProperty(key, defaultValue.ToString()); |
|||
return bool.TryParse(value, out var result) ? result : defaultValue; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class AvaloniaNameGenerator : INameGenerator |
|||
{ |
|||
private readonly ViewFileNamingStrategy _naming; |
|||
private readonly IGlobPattern _pathPattern; |
|||
private readonly IGlobPattern _namespacePattern; |
|||
private readonly IViewResolver _classes; |
|||
private readonly INameResolver _names; |
|||
private readonly ICodeGenerator _code; |
|||
|
|||
public AvaloniaNameGenerator( |
|||
ViewFileNamingStrategy naming, |
|||
IGlobPattern pathPattern, |
|||
IGlobPattern namespacePattern, |
|||
IViewResolver classes, |
|||
INameResolver names, |
|||
ICodeGenerator code) |
|||
{ |
|||
_naming = naming; |
|||
_pathPattern = pathPattern; |
|||
_namespacePattern = namespacePattern; |
|||
_classes = classes; |
|||
_names = names; |
|||
_code = code; |
|||
} |
|||
|
|||
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles) |
|||
{ |
|||
var resolveViews = |
|||
from file in additionalFiles |
|||
where (file.Path.EndsWith(".xaml") || |
|||
file.Path.EndsWith(".paml") || |
|||
file.Path.EndsWith(".axaml")) && |
|||
_pathPattern.Matches(file.Path) |
|||
let xaml = file.GetText()!.ToString() |
|||
let view = _classes.ResolveView(xaml) |
|||
where view != null && _namespacePattern.Matches(view.Namespace) |
|||
select view; |
|||
|
|||
var query = |
|||
from view in resolveViews |
|||
let names = _names.ResolveNames(view.Xaml) |
|||
let code = _code.GenerateCode(view.ClassName, view.Namespace, view.XamlType, names) |
|||
let fileName = ResolveViewFileName(view, _naming) |
|||
select new GeneratedPartialClass(fileName, code); |
|||
|
|||
return query.ToList(); |
|||
} |
|||
|
|||
private static string ResolveViewFileName(ResolvedView view, ViewFileNamingStrategy strategy) => strategy switch |
|||
{ |
|||
ViewFileNamingStrategy.ClassName => $"{view.ClassName}.g.cs", |
|||
ViewFileNamingStrategy.NamespaceAndClassName => $"{view.Namespace}.{view.ClassName}.g.cs", |
|||
_ => throw new ArgumentOutOfRangeException(nameof(strategy), strategy, "Unknown naming strategy!") |
|||
}; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
using System; |
|||
using Avalonia.Generators.Common; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using Avalonia.Generators.Compiler; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
[Generator] |
|||
public class AvaloniaNameSourceGenerator : ISourceGenerator |
|||
{ |
|||
public void Initialize(GeneratorInitializationContext context) { } |
|||
|
|||
public void Execute(GeneratorExecutionContext context) |
|||
{ |
|||
try |
|||
{ |
|||
var generator = CreateNameGenerator(context); |
|||
if (generator is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var partials = generator.GenerateNameReferences(context.AdditionalFiles); |
|||
foreach (var (fileName, content) in partials) context.AddSource(fileName, content); |
|||
} |
|||
catch (Exception exception) |
|||
{ |
|||
context.ReportNameGeneratorUnhandledError(exception); |
|||
} |
|||
} |
|||
|
|||
private static INameGenerator CreateNameGenerator(GeneratorExecutionContext context) |
|||
{ |
|||
var options = new GeneratorOptions(context); |
|||
if (!options.AvaloniaNameGeneratorIsEnabled) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var types = new RoslynTypeSystem((CSharpCompilation)context.Compilation); |
|||
ICodeGenerator generator = options.AvaloniaNameGeneratorBehavior switch { |
|||
Behavior.OnlyProperties => new OnlyPropertiesCodeGenerator(), |
|||
Behavior.InitializeComponent => new InitializeComponentCodeGenerator(types), |
|||
_ => throw new ArgumentOutOfRangeException() |
|||
}; |
|||
|
|||
var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); |
|||
return new AvaloniaNameGenerator( |
|||
options.AvaloniaNameGeneratorViewFileNamingStrategy, |
|||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByPath), |
|||
new GlobPatternGroup(options.AvaloniaNameGeneratorFilterByNamespace), |
|||
new XamlXViewResolver(types, compiler, true, |
|||
type => context.ReportNameGeneratorInvalidType(type), |
|||
error => context.ReportNameGeneratorUnhandledError(error)), |
|||
new XamlXNameResolver(options.AvaloniaNameGeneratorClassFieldModifier), |
|||
generator); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal interface INameGenerator |
|||
{ |
|||
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles); |
|||
} |
|||
|
|||
internal record GeneratedPartialClass(string FileName, string Content); |
|||
@ -0,0 +1,83 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class InitializeComponentCodeGenerator: ICodeGenerator |
|||
{ |
|||
private readonly bool _diagnosticsAreConnected; |
|||
private const string AttachDevToolsCodeBlock = @"
|
|||
#if DEBUG
|
|||
if (attachDevTools) |
|||
{ |
|||
this.AttachDevTools(); |
|||
} |
|||
#endif
|
|||
";
|
|||
private const string AttachDevToolsParameterDocumentation |
|||
= @" /// <param name=""attachDevTools"">Should the dev tools be attached.</param>
|
|||
";
|
|||
|
|||
public InitializeComponentCodeGenerator(IXamlTypeSystem types) |
|||
{ |
|||
_diagnosticsAreConnected = types.FindAssembly("Avalonia.Diagnostics") != null; |
|||
} |
|||
|
|||
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var properties = new List<string>(); |
|||
var initializations = new List<string>(); |
|||
foreach (var resolvedName in names) |
|||
{ |
|||
var (typeName, name, fieldModifier) = resolvedName; |
|||
properties.Add($" {fieldModifier} {typeName} {name};"); |
|||
initializations.Add($" {name} = this.FindNameScope()?.Find<{typeName}>(\"{name}\");"); |
|||
} |
|||
|
|||
var attachDevTools = _diagnosticsAreConnected && IsWindow(xamlType); |
|||
|
|||
return $@"// <auto-generated />
|
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace {nameSpace} |
|||
{{ |
|||
partial class {className} |
|||
{{ |
|||
{string.Join("\n", properties)} |
|||
|
|||
/// <summary>
|
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced).
|
|||
/// </summary>
|
|||
/// <param name=""loadXaml"">Should the XAML be loaded into the component.</param>
|
|||
{(attachDevTools ? AttachDevToolsParameterDocumentation : string.Empty)} |
|||
public void InitializeComponent(bool loadXaml = true{(attachDevTools ? ", bool attachDevTools = true" : string.Empty)}) |
|||
{{ |
|||
if (loadXaml) |
|||
{{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
}} |
|||
{(attachDevTools ? AttachDevToolsCodeBlock : string.Empty)} |
|||
{string.Join("\n", initializations)} |
|||
}} |
|||
}} |
|||
}} |
|||
";
|
|||
} |
|||
|
|||
private static bool IsWindow(IXamlType xamlType) |
|||
{ |
|||
var type = xamlType; |
|||
bool isWindow; |
|||
do |
|||
{ |
|||
isWindow = type.FullName == "Avalonia.Controls.Window"; |
|||
type = type.BaseType; |
|||
} while (!isWindow && type != null); |
|||
|
|||
return isWindow; |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Generators.Common.Domain; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal class OnlyPropertiesCodeGenerator : ICodeGenerator |
|||
{ |
|||
public string GenerateCode(string className, string nameSpace, IXamlType xamlType, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var namedControls = names |
|||
.Select(info => " " + |
|||
$"{info.FieldModifier} {info.TypeName} {info.Name} => " + |
|||
$"this.FindNameScope()?.Find<{info.TypeName}>(\"{info.Name}\");") |
|||
.ToList(); |
|||
var lines = string.Join("\n", namedControls); |
|||
return $@"// <auto-generated />
|
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace {nameSpace} |
|||
{{ |
|||
partial class {className} |
|||
{{ |
|||
{lines} |
|||
}} |
|||
}} |
|||
";
|
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
namespace Avalonia.Generators.NameGenerator; |
|||
|
|||
internal enum Options |
|||
{ |
|||
Public = 0, |
|||
Private = 1, |
|||
Internal = 2, |
|||
Protected = 3, |
|||
} |
|||
|
|||
internal enum Behavior |
|||
{ |
|||
OnlyProperties = 0, |
|||
InitializeComponent = 1, |
|||
} |
|||
|
|||
internal enum ViewFileNamingStrategy |
|||
{ |
|||
ClassName = 0, |
|||
NamespaceAndClassName = 1, |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"profiles": { |
|||
"Profile 1": { |
|||
"commandName": "DebugRoslynComponent", |
|||
"targetProject": "..\\..\\..\\samples\\Generators.Sandbox\\Generators.Sandbox.csproj" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,209 @@ |
|||
[](https://www.nuget.org/packages/XamlNameReferenceGenerator) [](https://www.nuget.org/packages/XamlNameReferenceGenerator)    |
|||
|
|||
### C# `SourceGenerator` for Typed Avalonia `x:Name` References |
|||
|
|||
This is a [C# `SourceGenerator`](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) built for generating strongly-typed references to controls with `x:Name` (or just `Name`) attributes declared in XAML (or, in `.axaml`). The source generator will look for the `xaml` (or `axaml`) file with the same name as your partial C# class that is a subclass of `Avalonia.INamed` and parses the XAML markup, finds all XAML tags with `x:Name` attributes and generates the C# code. |
|||
|
|||
### Getting Started |
|||
|
|||
In order to get started, just install the NuGet package: |
|||
|
|||
``` |
|||
dotnet add package XamlNameReferenceGenerator |
|||
``` |
|||
|
|||
Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you can reference the generator as such: |
|||
|
|||
```xml |
|||
<ItemGroup> |
|||
<!-- Remember to ensure XAML files are included via <AdditionalFiles>, |
|||
otherwise C# source generator won't see XAML files. --> |
|||
<AdditionalFiles Include="**\*.xaml"/> |
|||
<ProjectReference Include="..\Avalonia.NameGenerator\Avalonia.NameGenerator.csproj" |
|||
OutputItemType="Analyzer" |
|||
ReferenceOutputAssembly="false" /> |
|||
</ItemGroup> |
|||
``` |
|||
|
|||
### Usage |
|||
|
|||
After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: |
|||
|
|||
```xml |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.SignUpView"> |
|||
<TextBox x:Name="UserNameTextBox" x:FieldModifier="public" /> |
|||
</Window> |
|||
``` |
|||
|
|||
A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: |
|||
|
|||
```cs |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
public partial class SignUpView : Window |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
// This method is generated. Call it before accessing any |
|||
// of the generated properties. The 'UserNameTextBox' |
|||
// property is also generated. |
|||
InitializeComponent(); |
|||
UserNameTextBox.Text = "Joseph"; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
<img src="https://hsto.org/getpro/habr/post_images/d9f/4aa/a1e/d9f4aaa1eb450f5dd2fca66631bc16a0.gif" /> |
|||
|
|||
### Why do I need this? |
|||
|
|||
The typed `x:Name` references might be useful if you decide to use e.g. [ReactiveUI code-behind bindings](https://www.reactiveui.net/docs/handbook/data-binding/): |
|||
|
|||
```cs |
|||
// UserNameValidation and PasswordValidation are auto generated. |
|||
public partial class SignUpView : ReactiveWindow<SignUpViewModel> |
|||
{ |
|||
public SignUpView() |
|||
{ |
|||
InitializeComponent(); |
|||
this.WhenActivated(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); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Advanced Usage |
|||
|
|||
> Never keep a method named `InitializeComponent` in your code-behind view class if you are using the generator with `AvaloniaNameGeneratorBehavior` set to `InitializeComponent` (this is the default value). The private `InitializeComponent` method declared in your code-behind class hides the `InitializeComponent` method generated by `Avalonia.NameGenerator`, see [Issue 69](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/69). If you wish to use your own `InitializeComponent` method (not the generated one), set `AvaloniaNameGeneratorBehavior` to `OnlyProperties`. |
|||
|
|||
The `x:Name` generator can be configured via MsBuild properties that you can put into your C# project file (`.csproj`). Using such options, you can configure the generator behavior, the default field modifier, namespace and path filters. The generator supports the following options: |
|||
|
|||
- `AvaloniaNameGeneratorBehavior` |
|||
Possible values: `OnlyProperties`, `InitializeComponent` |
|||
Default value: `InitializeComponent` |
|||
Determines if the generator should generate get-only properties, or the `InitializeComponent` method. |
|||
|
|||
- `AvaloniaNameGeneratorDefaultFieldModifier` |
|||
Possible values: `internal`, `public`, `private`, `protected` |
|||
Default value: `internal` |
|||
The default field modifier that should be used when there is no `x:FieldModifier` directive specified. |
|||
|
|||
- `AvaloniaNameGeneratorFilterByPath` |
|||
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
|||
Default value: `*` |
|||
The generator will process only XAML files with paths matching the specified glob pattern(s). |
|||
Example: `*/Views/*View.xaml`, `*View.axaml;*Control.axaml` |
|||
|
|||
- `AvaloniaNameGeneratorFilterByNamespace` |
|||
Posssible format: `glob_pattern`, `glob_pattern;glob_pattern` |
|||
Default value: `*` |
|||
The generator will process only XAML files with base classes' namespaces matching the specified glob pattern(s). |
|||
Example: `MyApp.Presentation.*`, `MyApp.Presentation.Views;MyApp.Presentation.Controls` |
|||
|
|||
- `AvaloniaNameGeneratorViewFileNamingStrategy` |
|||
Possible values: `ClassName`, `NamespaceAndClassName` |
|||
Default value: `NamespaceAndClassName` |
|||
Determines how the automatically generated view files should be [named](https://github.com/AvaloniaUI/Avalonia.NameGenerator/issues/92). |
|||
|
|||
The default values are given by: |
|||
|
|||
```xml |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<AvaloniaNameGeneratorBehavior>InitializeComponent</AvaloniaNameGeneratorBehavior> |
|||
<AvaloniaNameGeneratorDefaultFieldModifier>internal</AvaloniaNameGeneratorDefaultFieldModifier> |
|||
<AvaloniaNameGeneratorFilterByPath>*</AvaloniaNameGeneratorFilterByPath> |
|||
<AvaloniaNameGeneratorFilterByNamespace>*</AvaloniaNameGeneratorFilterByNamespace> |
|||
<AvaloniaNameGeneratorViewFileNamingStrategy>NamespaceAndClassName</AvaloniaNameGeneratorViewFileNamingStrategy> |
|||
</PropertyGroup> |
|||
<!-- ... --> |
|||
</Project> |
|||
``` |
|||
|
|||
 |
|||
|
|||
### What do the generated sources look like? |
|||
|
|||
For [`SignUpView`](https://github.com/avaloniaui/Avalonia.NameGenerator/blob/main/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml), we get the following generated output when the source generator is in the `InitializeComponent` mode: |
|||
|
|||
```cs |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox; |
|||
public global::Avalonia.Controls.TextBlock UserNameValidation; |
|||
private global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation; |
|||
internal global::Avalonia.Controls.ListBox AwesomeListView; |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation; |
|||
|
|||
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
// This will be added only if you install Avalonia.Diagnostics. |
|||
#if DEBUG |
|||
if (attachDevTools) |
|||
{ |
|||
this.AttachDevTools(); |
|||
} |
|||
#endif |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
|||
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
|||
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
If you enable the `OnlyProperties` source generator mode, you get: |
|||
|
|||
```cs |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.NameGenerator.Sandbox.Views |
|||
{ |
|||
partial class SignUpView |
|||
{ |
|||
internal global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.NameGenerator.Sandbox.Controls.CustomTextBox>("UserNameTextBox"); |
|||
public global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
private global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
``` |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace>Avalonia.Generators.Tests</RootNamespace> |
|||
<IsTestProject>true</IsTestProject> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" /> |
|||
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> |
|||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Views\*.xml" /> |
|||
<EmbeddedResource Include="OnlyProperties\GeneratedCode\*.txt" /> |
|||
<EmbeddedResource Include="InitializeComponent\GeneratedInitializeComponent\*.txt" /> |
|||
<EmbeddedResource Include="InitializeComponent\GeneratedDevTools\*.txt" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
|||
<Import Project="..\..\build\XUnit.props" /> |
|||
<Import Project="..\..\build\SharedVersion.props" /> |
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
using Avalonia.Generators.Common; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Generators.Tests; |
|||
|
|||
public class GlobPatternTests |
|||
{ |
|||
[Theory] |
|||
[InlineData("*", "anything", true)] |
|||
[InlineData("", "anything", false)] |
|||
[InlineData("Views/*", "Views/SignUpView.xaml", true)] |
|||
[InlineData("Views/*", "Extensions/SignUpView.xaml", false)] |
|||
[InlineData("*SignUpView*", "Extensions/SignUpView.xaml", true)] |
|||
[InlineData("*SignUpView.paml", "Extensions/SignUpView.xaml", false)] |
|||
[InlineData("*.xaml", "Extensions/SignUpView.xaml", true)] |
|||
public void Should_Match_Glob_Expressions(string pattern, string value, bool matches) |
|||
{ |
|||
Assert.Equal(matches, new GlobPattern(pattern).Matches(value)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("Views/SignUpView.xaml", true, new[] { "*.xaml", "Extensions/*" })] |
|||
[InlineData("Extensions/SignUpView.paml", true, new[] { "*.xaml", "Extensions/*" })] |
|||
[InlineData("Extensions/SignUpView.paml", false, new[] { "*.xaml", "Views/*" })] |
|||
[InlineData("anything", true, new[] { "*", "*" })] |
|||
[InlineData("anything", false, new[] { "", "" })] |
|||
public void Should_Match_Glob_Pattern_Groups(string value, bool matches, string[] patterns) |
|||
{ |
|||
Assert.Equal(matches, new GlobPatternGroup(patterns).Matches(value)); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
/// <param name="attachDevTools">Should the dev tools be attached.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
#if DEBUG |
|||
if (attachDevTools) |
|||
{ |
|||
this.AttachDevTools(); |
|||
} |
|||
#endif |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost; |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost; |
|||
internal global::Controls.CustomTextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
ClrNamespaceRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost"); |
|||
UriRoutedViewHost = this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost"); |
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
internal global::Avalonia.Controls.ListBox NamedListBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
NamedListBox = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
public global::Avalonia.Controls.TextBox FirstNameTextBox; |
|||
public global::Avalonia.Controls.TextBox LastNameTextBox; |
|||
protected global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
internal global::Avalonia.Controls.Button RegisterButton; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
FirstNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox"); |
|||
LastNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
RegisterButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; |
|||
|
|||
public static class InitializeComponentCode |
|||
{ |
|||
public const string NamedControl = "NamedControl.txt"; |
|||
public const string NamedControls = "NamedControls.txt"; |
|||
public const string XNamedControl = "xNamedControl.txt"; |
|||
public const string XNamedControls = "xNamedControls.txt"; |
|||
public const string NoNamedControls = "NoNamedControls.txt"; |
|||
public const string CustomControls = "CustomControls.txt"; |
|||
public const string DataTemplates = "DataTemplates.txt"; |
|||
public const string SignUpView = "SignUpView.txt"; |
|||
public const string FieldModifier = "FieldModifier.txt"; |
|||
public const string AttachedProps = "AttachedProps.txt"; |
|||
public const string AttachedPropsWithDevTools = "AttachedPropsWithDevTools.txt"; |
|||
public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; |
|||
|
|||
public static async Task<string> Load(string generatedCodeResourceName) |
|||
{ |
|||
var assembly = typeof(XamlXNameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.Contains("InitializeComponent") && |
|||
name.Contains("GeneratedInitializeComponent") && |
|||
name.EndsWith(generatedCodeResourceName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
|
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Controls.CustomTextBox UserNameTextBox; |
|||
internal global::Avalonia.Controls.TextBlock UserNameValidation; |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation; |
|||
internal global::Avalonia.Controls.ListBox AwesomeListView; |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox; |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation; |
|||
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
UserNameValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
PasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
AwesomeListView = this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
|||
ConfirmPasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
ConfirmPasswordValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
SignUpButtonDescription = this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
CompoundValidation = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox; |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox; |
|||
internal global::Avalonia.Controls.Button SignUpButton; |
|||
|
|||
/// <summary> |
|||
/// Wires up the controls and optionally loads XAML markup and attaches dev tools (if Avalonia.Diagnostics package is referenced). |
|||
/// </summary> |
|||
/// <param name="loadXaml">Should the XAML be loaded into the component.</param> |
|||
|
|||
public void InitializeComponent(bool loadXaml = true) |
|||
{ |
|||
if (loadXaml) |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
UserNameTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
PasswordTextBox = this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
SignUpButton = this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Generators.Common; |
|||
using Avalonia.Generators.Compiler; |
|||
using Avalonia.Generators.NameGenerator; |
|||
using Avalonia.Generators.Tests.InitializeComponent.GeneratedInitializeComponent; |
|||
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
|||
using Avalonia.Generators.Tests.Views; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Generators.Tests.InitializeComponent; |
|||
|
|||
public class InitializeComponentTests |
|||
{ |
|||
[Theory] |
|||
[InlineData(InitializeComponentCode.NamedControl, View.NamedControl, false)] |
|||
[InlineData(InitializeComponentCode.NamedControls, View.NamedControls, false)] |
|||
[InlineData(InitializeComponentCode.XNamedControl, View.XNamedControl, false)] |
|||
[InlineData(InitializeComponentCode.XNamedControls, View.XNamedControls, false)] |
|||
[InlineData(InitializeComponentCode.NoNamedControls, View.NoNamedControls, false)] |
|||
[InlineData(InitializeComponentCode.CustomControls, View.CustomControls, false)] |
|||
[InlineData(InitializeComponentCode.DataTemplates, View.DataTemplates, false)] |
|||
[InlineData(InitializeComponentCode.SignUpView, View.SignUpView, false)] |
|||
[InlineData(InitializeComponentCode.FieldModifier, View.FieldModifier, false)] |
|||
[InlineData(InitializeComponentCode.AttachedPropsWithDevTools, View.AttachedProps, true)] |
|||
[InlineData(InitializeComponentCode.AttachedProps, View.AttachedProps, false)] |
|||
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, true)] |
|||
[InlineData(InitializeComponentCode.ControlWithoutWindow, View.ControlWithoutWindow, false)] |
|||
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File( |
|||
string expectation, |
|||
string markup, |
|||
bool devToolsMode) |
|||
{ |
|||
var excluded = devToolsMode ? null : "Avalonia.Diagnostics"; |
|||
var compilation = |
|||
View.CreateAvaloniaCompilation(excluded) |
|||
.WithCustomTextBox(); |
|||
|
|||
var types = new RoslynTypeSystem(compilation); |
|||
var classResolver = new XamlXViewResolver( |
|||
types, |
|||
MiniCompiler.CreateDefault( |
|||
new RoslynTypeSystem(compilation), |
|||
MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
|||
|
|||
var xaml = await View.Load(markup); |
|||
var classInfo = classResolver.ResolveView(xaml); |
|||
var nameResolver = new XamlXNameResolver(); |
|||
var names = nameResolver.ResolveNames(classInfo.Xaml); |
|||
|
|||
var generator = new InitializeComponentCodeGenerator(types); |
|||
|
|||
var code = generator |
|||
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) |
|||
.Replace("\r", string.Empty); |
|||
|
|||
var expected = await InitializeComponentCode.Load(expectation); |
|||
|
|||
|
|||
CSharpSyntaxTree.ParseText(code); |
|||
Assert.Equal(expected.Replace("\r", string.Empty), code); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using Avalonia.Generators.Compiler; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Avalonia.Generators.Tests.Views; |
|||
using XamlX; |
|||
using XamlX.Parsers; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Generators.Tests; |
|||
|
|||
public class MiniCompilerTests |
|||
{ |
|||
private const string AvaloniaXaml = "<TextBlock xmlns='clr-namespace:Avalonia.Controls;assembly=Avalonia' />"; |
|||
private const string MiniClass = "namespace Example { public class Valid { public int Foo() => 21; } }"; |
|||
private const string MiniInvalidXaml = "<Invalid xmlns='clr-namespace:Example;assembly=Example' />"; |
|||
private const string MiniValidXaml = "<Valid xmlns='clr-namespace:Example;assembly=Example' />"; |
|||
|
|||
[Fact] |
|||
public void Should_Resolve_Types_From_Simple_Valid_Xaml_Markup() |
|||
{ |
|||
var xaml = XDocumentXamlParser.Parse(MiniValidXaml); |
|||
var compilation = CreateBasicCompilation(MiniClass); |
|||
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); |
|||
|
|||
Assert.NotNull(xaml.Root); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Throw_When_Unable_To_Resolve_Types_From_Simple_Invalid_Markup() |
|||
{ |
|||
var xaml = XDocumentXamlParser.Parse(MiniInvalidXaml); |
|||
var compilation = CreateBasicCompilation(MiniClass); |
|||
var compiler = MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)); |
|||
|
|||
Assert.Throws<XamlParseException>(() => compiler.Transform(xaml)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Resolve_Types_From_Simple_Avalonia_Markup() |
|||
{ |
|||
var xaml = XDocumentXamlParser.Parse(AvaloniaXaml); |
|||
var compilation = View.CreateAvaloniaCompilation(); |
|||
MiniCompiler.CreateDefault(new RoslynTypeSystem(compilation)).Transform(xaml); |
|||
|
|||
Assert.NotNull(xaml.Root); |
|||
} |
|||
|
|||
private static CSharpCompilation CreateBasicCompilation(string source) => |
|||
CSharpCompilation |
|||
.Create("BasicLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)) |
|||
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost ClrNamespaceRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("ClrNamespaceRoutedViewHost"); |
|||
internal global::Avalonia.ReactiveUI.RoutedViewHost UriRoutedViewHost => this.FindNameScope()?.Find<global::Avalonia.ReactiveUI.RoutedViewHost>("UriRoutedViewHost"); |
|||
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.ListBox NamedListBox => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("NamedListBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
public global::Avalonia.Controls.TextBox FirstNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("FirstNameTextBox"); |
|||
public global::Avalonia.Controls.TextBox LastNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("LastNameTextBox"); |
|||
protected global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
private global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.Button RegisterButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("RegisterButton"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
|||
|
|||
public static class OnlyPropertiesCode |
|||
{ |
|||
public const string NamedControl = "NamedControl.txt"; |
|||
public const string NamedControls = "NamedControls.txt"; |
|||
public const string XNamedControl = "xNamedControl.txt"; |
|||
public const string XNamedControls = "xNamedControls.txt"; |
|||
public const string NoNamedControls = "NoNamedControls.txt"; |
|||
public const string CustomControls = "CustomControls.txt"; |
|||
public const string DataTemplates = "DataTemplates.txt"; |
|||
public const string SignUpView = "SignUpView.txt"; |
|||
public const string AttachedProps = "AttachedProps.txt"; |
|||
public const string FieldModifier = "FieldModifier.txt"; |
|||
public const string ControlWithoutWindow = "ControlWithoutWindow.txt"; |
|||
|
|||
public static async Task<string> Load(string generatedCodeResourceName) |
|||
{ |
|||
var assembly = typeof(XamlXNameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.Contains("OnlyProperties") && name.EndsWith(generatedCodeResourceName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Controls.CustomTextBox UserNameTextBox => this.FindNameScope()?.Find<global::Controls.CustomTextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("UserNameValidation"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock PasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("PasswordValidation"); |
|||
internal global::Avalonia.Controls.ListBox AwesomeListView => this.FindNameScope()?.Find<global::Avalonia.Controls.ListBox>("AwesomeListView"); |
|||
internal global::Avalonia.Controls.TextBox ConfirmPasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("ConfirmPasswordTextBox"); |
|||
internal global::Avalonia.Controls.TextBlock ConfirmPasswordValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("ConfirmPasswordValidation"); |
|||
internal global::Avalonia.Controls.Documents.Run SignUpButtonDescription => this.FindNameScope()?.Find<global::Avalonia.Controls.Documents.Run>("SignUpButtonDescription"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
internal global::Avalonia.Controls.TextBlock CompoundValidation => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBlock>("CompoundValidation"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// <auto-generated /> |
|||
|
|||
using Avalonia.Controls; |
|||
|
|||
namespace Sample.App |
|||
{ |
|||
partial class SampleView |
|||
{ |
|||
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("UserNameTextBox"); |
|||
internal global::Avalonia.Controls.TextBox PasswordTextBox => this.FindNameScope()?.Find<global::Avalonia.Controls.TextBox>("PasswordTextBox"); |
|||
internal global::Avalonia.Controls.Button SignUpButton => this.FindNameScope()?.Find<global::Avalonia.Controls.Button>("SignUpButton"); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Generators.Common; |
|||
using Avalonia.Generators.Compiler; |
|||
using Avalonia.Generators.NameGenerator; |
|||
using Avalonia.Generators.Tests.OnlyProperties.GeneratedCode; |
|||
using Avalonia.Generators.Tests.Views; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Generators.Tests.OnlyProperties; |
|||
|
|||
public class OnlyPropertiesTests |
|||
{ |
|||
[Theory] |
|||
[InlineData(OnlyPropertiesCode.NamedControl, View.NamedControl)] |
|||
[InlineData(OnlyPropertiesCode.NamedControls, View.NamedControls)] |
|||
[InlineData(OnlyPropertiesCode.XNamedControl, View.XNamedControl)] |
|||
[InlineData(OnlyPropertiesCode.XNamedControls, View.XNamedControls)] |
|||
[InlineData(OnlyPropertiesCode.NoNamedControls, View.NoNamedControls)] |
|||
[InlineData(OnlyPropertiesCode.CustomControls, View.CustomControls)] |
|||
[InlineData(OnlyPropertiesCode.DataTemplates, View.DataTemplates)] |
|||
[InlineData(OnlyPropertiesCode.SignUpView, View.SignUpView)] |
|||
[InlineData(OnlyPropertiesCode.AttachedProps, View.AttachedProps)] |
|||
[InlineData(OnlyPropertiesCode.FieldModifier, View.FieldModifier)] |
|||
[InlineData(OnlyPropertiesCode.ControlWithoutWindow, View.ControlWithoutWindow)] |
|||
public async Task Should_Generate_FindControl_Refs_From_Avalonia_Markup_File(string expectation, string markup) |
|||
{ |
|||
var compilation = |
|||
View.CreateAvaloniaCompilation() |
|||
.WithCustomTextBox(); |
|||
|
|||
var classResolver = new XamlXViewResolver( |
|||
new RoslynTypeSystem(compilation), |
|||
MiniCompiler.CreateDefault( |
|||
new RoslynTypeSystem(compilation), |
|||
MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
|||
|
|||
var xaml = await View.Load(markup); |
|||
var classInfo = classResolver.ResolveView(xaml); |
|||
var nameResolver = new XamlXNameResolver(); |
|||
var names = nameResolver.ResolveNames(classInfo.Xaml); |
|||
|
|||
var generator = new OnlyPropertiesCodeGenerator(); |
|||
var code = generator |
|||
.GenerateCode("SampleView", "Sample.App", classInfo.XamlType, names) |
|||
.Replace("\r", string.Empty); |
|||
|
|||
var expected = await OnlyPropertiesCode.Load(expectation); |
|||
CSharpSyntaxTree.ParseText(code); |
|||
Assert.Equal(expected.Replace("\r", string.Empty), code); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:rxui="http://reactiveui.net" |
|||
x:Class="Sample.App.AttachedProps" |
|||
Design.Width="300"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
@ -0,0 +1,10 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:rxui="http://reactiveui.net" |
|||
x:Class="Sample.App.ControlWithoutWindow" |
|||
Design.Width="300"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
|||
xmlns:controls="clr-namespace:Controls" |
|||
x:Class="Sample.App.CustomControls" |
|||
xmlns:rxui="http://reactiveui.net"> |
|||
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" /> |
|||
<rxui:RoutedViewHost Name="UriRoutedViewHost" /> |
|||
<controls:CustomTextBox Name="UserNameTextBox" /> |
|||
<controls:EvilControl Name="EvilName" /> |
|||
</Window> |
|||
@ -0,0 +1,18 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.DataTemplates"> |
|||
<StackPanel> |
|||
<TextBox x:Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<ListBox Name="NamedListBox"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate x:Name="NamedDataTemplate"> |
|||
<TextBox x:Name="TemplatedTextBox" |
|||
Watermark="Templated input" |
|||
UseFloatingWatermark="True" /> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,28 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.FieldModifier"> |
|||
<StackPanel> |
|||
<TextBox Name="FirstNameTextBox" |
|||
x:FieldModifier="Public" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="LastNameTextBox" |
|||
x:FieldModifier="public" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="PasswordTextBox" |
|||
x:FieldModifier="protected" |
|||
Watermark="Password input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="ConfirmPasswordTextBox" |
|||
x:FieldModifier="private" |
|||
Watermark="Password input" |
|||
UseFloatingWatermark="True" /> |
|||
<Button Name="SignUpButton" |
|||
x:FieldModifier="NotPublic" |
|||
Content="Sign up" /> |
|||
<Button Name="RegisterButton" |
|||
x:FieldModifier="Nonsense" |
|||
Content="Register" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,7 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.NamedControl"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
@ -0,0 +1,14 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.NamedControls"> |
|||
<StackPanel> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
<TextBox Name="PasswordTextBox" |
|||
Watermark="Password input" |
|||
UseFloatingWatermark="True" /> |
|||
<Button Name="SignUpButton" |
|||
Content="Sign up" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,6 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.NoNamedControls"> |
|||
<TextBox Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
@ -0,0 +1,52 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="clr-namespace:Controls" |
|||
x:Class="Sample.App.SignUpView"> |
|||
<StackPanel> |
|||
<controls:CustomTextBox Margin="0 10 0 0" |
|||
Name="UserNameTextBox" |
|||
Watermark="Please, enter user name..." |
|||
UseFloatingWatermark="True" /> |
|||
<TextBlock Name="UserNameValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBox Margin="0 10 0 0" |
|||
Name="PasswordTextBox" |
|||
Watermark="Please, enter your password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock Name="PasswordValidation" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<ListBox x:Name="AwesomeListView"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock x:Name="MeaningLessName" Text="{Binding}" /> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
<TextBox Margin="0 10 0 0" |
|||
x:Name="ConfirmPasswordTextBox" |
|||
Watermark="Please, confirm the password..." |
|||
UseFloatingWatermark="True" |
|||
PasswordChar="*" /> |
|||
<TextBlock x:Name="ConfirmPasswordValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
<TextBlock> |
|||
<TextBlock.Inlines> |
|||
<InlineCollection> |
|||
<Run x:Name="SignUpButtonDescription" /> |
|||
</InlineCollection> |
|||
</TextBlock.Inlines> |
|||
</TextBlock> |
|||
<Button Margin="0 10 0 5" |
|||
Content="Sign up" |
|||
x:Name="SignUpButton" /> |
|||
<TextBlock x:Name="CompoundValidation" |
|||
TextWrapping="Wrap" |
|||
Foreground="Red" |
|||
FontSize="12" /> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,76 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
|
|||
namespace Avalonia.Generators.Tests.Views; |
|||
|
|||
public static class View |
|||
{ |
|||
public const string NamedControl = "NamedControl.xml"; |
|||
public const string NamedControls = "NamedControls.xml"; |
|||
public const string XNamedControl = "xNamedControl.xml"; |
|||
public const string XNamedControls = "xNamedControls.xml"; |
|||
public const string NoNamedControls = "NoNamedControls.xml"; |
|||
public const string CustomControls = "CustomControls.xml"; |
|||
public const string DataTemplates = "DataTemplates.xml"; |
|||
public const string SignUpView = "SignUpView.xml"; |
|||
public const string AttachedProps = "AttachedProps.xml"; |
|||
public const string FieldModifier = "FieldModifier.xml"; |
|||
public const string ControlWithoutWindow = "ControlWithoutWindow.xml"; |
|||
public const string ViewWithGenericBaseView = "ViewWithGenericBaseView.xml"; |
|||
|
|||
public static async Task<string> Load(string viewName) |
|||
{ |
|||
var assembly = typeof(XamlXNameResolverTests).Assembly; |
|||
var fullResourceName = assembly |
|||
.GetManifestResourceNames() |
|||
.First(name => name.EndsWith(viewName)); |
|||
|
|||
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
|||
using var reader = new StreamReader(stream!); |
|||
return await reader.ReadToEndAsync(); |
|||
} |
|||
|
|||
public static CSharpCompilation CreateAvaloniaCompilation(string excludedPattern = null) |
|||
{ |
|||
var compilation = CSharpCompilation |
|||
.Create("AvaloniaLib", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
|||
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)); |
|||
|
|||
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location; |
|||
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation); |
|||
var avaloniaAssemblyReferences = Directory |
|||
.EnumerateFiles(avaloniaAssemblyDirectory!) |
|||
.Where(file => file.EndsWith(".dll") && |
|||
file.Contains("Avalonia") && |
|||
(string.IsNullOrWhiteSpace(excludedPattern) || !file.Contains(excludedPattern))) |
|||
.Select(file => MetadataReference.CreateFromFile(file)) |
|||
.ToList(); |
|||
|
|||
return compilation.AddReferences(avaloniaAssemblyReferences); |
|||
} |
|||
|
|||
public static CSharpCompilation WithCustomTextBox(this CSharpCompilation compilation) => |
|||
compilation.AddSyntaxTrees( |
|||
CSharpSyntaxTree.ParseText( |
|||
"using Avalonia.Controls;" + |
|||
"namespace Controls {" + |
|||
" public class CustomTextBox : TextBox { }" + |
|||
" public class EvilControl { }" + |
|||
"}")); |
|||
|
|||
public static CSharpCompilation WithBaseView(this CSharpCompilation compilation) => |
|||
compilation.AddSyntaxTrees( |
|||
CSharpSyntaxTree.ParseText( |
|||
"using Avalonia.Controls;" + |
|||
"namespace Sample.App { public class BaseView<TViewModel> : UserControl { } }")); |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<local:BaseView |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.ViewWithGenericBaseView" |
|||
Design.Width="300" |
|||
xmlns:sys="clr-namespace:System" |
|||
x:TypeArguments="sys:String" |
|||
x:Name="Root" |
|||
xmlns:local="clr-namespace:Sample.App"> |
|||
<Grid> |
|||
<local:BaseView x:Name="NotAsRootNode" x:TypeArguments="sys:Int32" /> |
|||
</Grid> |
|||
</local:BaseView> |
|||
@ -0,0 +1,7 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.xNamedControl"> |
|||
<TextBox x:Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue