diff --git a/WinUI.TableView.slnx b/WinUI.TableView.slnx index 64845ab8..a612bb5e 100644 --- a/WinUI.TableView.slnx +++ b/WinUI.TableView.slnx @@ -1,11 +1,16 @@ + + + + + diff --git a/generators/AnalyzerReleases.Shipped.md b/generators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..42695a22 --- /dev/null +++ b/generators/AnalyzerReleases.Shipped.md @@ -0,0 +1,11 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Release%20Tracking.md + +## Release 1.0.0 +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------ +TV0001 | WinUI.TableView.SourceGenerators | Error | TableView must define x:Name or Name to enable generated connector code. +TV0002 | WinUI.TableView.SourceGenerators | Error | Unable to resolve TableView item type from x:Bind ItemsSource. +TV0003 | WinUI.TableView.SourceGenerators | Error | Window roots with generated TableView connectors must call ConnectTableViews() manually after InitializeComponent(). +TV0004 | WinUI.TableView.SourceGenerators | Error | TableView member path could not be resolved for the inferred item type. diff --git a/generators/AnalyzerReleases.Unshipped.md b/generators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..8a8966e0 --- /dev/null +++ b/generators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,6 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Release%20Tracking.md + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------ diff --git a/generators/TableViewBindingProviderGenerator.Definitions.cs b/generators/TableViewBindingProviderGenerator.Definitions.cs new file mode 100644 index 00000000..9e2e55a4 --- /dev/null +++ b/generators/TableViewBindingProviderGenerator.Definitions.cs @@ -0,0 +1,128 @@ +using Microsoft.CodeAnalysis; +using System.Text.RegularExpressions; +namespace WinUI.TableView.SourceGenerators; +public sealed partial class TableViewBindingProviderGenerator +{ + private static readonly DiagnosticDescriptor TableViewNameRequiredDescriptor = + new( + id: "TV0001", + title: "TableView requires a name for source generation", + messageFormat: "TableView in '{0}' must define x:Name or Name to enable generated code", + category: "WinUI.TableView.SourceGenerators", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnableToResolveItemsTypeDescriptor = + new( + id: "TV0002", + title: "Unable to resolve TableView item type", + messageFormat: "TableView in '{0}' uses x:Bind ItemsSource '{1}', but the item type could not be resolved to a typed generic collection", + category: "WinUI.TableView.SourceGenerators", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor WindowRequiresManualConnectDescriptor = + new( + id: "TV0003", + title: "Window containing TableView must call ConnectTableViews", + messageFormat: "Class '{0}' contains TableView in a Window. Call ConnectTableViews() in code-behind after InitializeComponent().", + category: "WinUI.TableView.SourceGenerators", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnableToResolveMemberPathDescriptor = + new( + id: "TV0004", + title: "Unable to resolve TableView member path", + messageFormat: "TableView in '{0}' could not resolve member path '{1}' for item type '{2}'", + category: "WinUI.TableView.SourceGenerators", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly Regex XamlClassRegex = + new( + @"x:Class\s*=\s*[""'](?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex TableViewTagRegex = + new( + @"<\s*(?:(?[A-Za-z_][A-Za-z0-9_]*)\:)?TableView(?=\s|/|>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex XmlnsPrefixRegex = + new( + @"xmlns\:(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*[""']using:WinUI\.TableView(?:;[^""']*)?[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex XmlnsDefaultRegex = + new( + @"xmlns\s*=\s*[""']using:WinUI\.TableView(?:;[^""']*)?[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex TableViewNameRegex = + new( + @"(?:x:Name|Name)\s*=\s*[""'](?[A-Za-z_][A-Za-z0-9_]*)[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ItemsSourceBindingRegex = + new( + @"ItemsSource\s*=\s*[""']\{(?x:Bind|Binding)\s*(?[^}]*)\}[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SortMemberPathRegex = + new( + @"SortMemberPath\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ClipboardBindingRegex = + new( + @"ClipboardContentBinding\s*=\s*[""']\{Binding\s*(?[^}]*)\}[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ContentBindingRegex = + new( + @"(?[^}]*)\}[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex DisplayMemberPathRegex = + new( + @"DisplayMemberPath\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + + private static readonly Regex CellBindingRegex = + new( + @"\bBinding\s*=\s*[""']\{Binding\s*(?[^}]*)\}[""']", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex BindingPathTokenRegex = + new( + @"(?:^|,)\s*Path\s*=\s*(?[^,\s]+)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex BindingModeTokenRegex = + new( + @"(?:^|,)\s*Mode\s*=\s*(?[^,\s]+)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex NonSimpleBindingTokenRegex = + new( + @"(?:^|,)\s*(?:Source|RelativeSource|ElementName)\s*=", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ColumnTagRegex = + new( + @"<\s*(?:(?[A-Za-z_][A-Za-z0-9_]*)\:)?(?[A-Za-z_][A-Za-z0-9_]*Column)\b(?[^>]*)>", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex CSharpMemberPathRegex = + new( + @"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$", + RegexOptions.Compiled); + + private static readonly SymbolDisplayFormat FullyQualifiedNonAliasedTypeFormat = + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions + & ~SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + +} diff --git a/generators/TableViewBindingProviderGenerator.Models.cs b/generators/TableViewBindingProviderGenerator.Models.cs new file mode 100644 index 00000000..76a0432f --- /dev/null +++ b/generators/TableViewBindingProviderGenerator.Models.cs @@ -0,0 +1,666 @@ +using System.Collections.Immutable; + +namespace WinUI.TableView.SourceGenerators; + +public sealed partial class TableViewBindingProviderGenerator +{ + private readonly struct ParsedXamlInfo + { + public ParsedXamlInfo( + string? namespaceName, + string className, + string fullyQualifiedClassName, + string hintName, + bool isWindowRoot, + ImmutableArray tableViews) + { + NamespaceName = namespaceName; + ClassName = className; + FullyQualifiedClassName = fullyQualifiedClassName; + HintName = hintName; + IsWindowRoot = isWindowRoot; + TableViews = tableViews; + } + + public string? NamespaceName { get; } + + public string ClassName { get; } + + public string FullyQualifiedClassName { get; } + + public string HintName { get; } + + public bool IsWindowRoot { get; } + + public ImmutableArray TableViews { get; } + } + + /// + /// Raw TableView data parsed from XAML text before Roslyn line mapping. + /// + private readonly struct TableViewInfoRaw + { + public TableViewInfoRaw( + string? tableViewName, + string tableViewLine, + string? itemsSourceXBindPath, + ImmutableArray sortMemberPaths, + ImmutableArray displayMemberPaths, + ImmutableArray bindingPaths, + ImmutableArray clipboardPaths, + ImmutableArray contentPaths, + ImmutableArray columnDefinitions, + ImmutableArray sortMemberPathSpans, + ImmutableArray displayMemberPathSpans, + ImmutableArray bindingPathSpans, + ImmutableArray clipboardPathSpans, + ImmutableArray contentPathSpans, + int itemsSourceSpanStart, + int itemsSourceSpanLength) + { + TableViewName = tableViewName; + TableViewLine = tableViewLine; + ItemsSourceXBindPath = itemsSourceXBindPath; + SortMemberPaths = sortMemberPaths; + DisplayMemberPaths = displayMemberPaths; + BindingPaths = bindingPaths; + ClipboardPaths = clipboardPaths; + ContentPaths = contentPaths; + ColumnDefinitions = columnDefinitions; + SortMemberPathSpans = sortMemberPathSpans; + DisplayMemberPathSpans = displayMemberPathSpans; + BindingPathSpans = bindingPathSpans; + ClipboardPathSpans = clipboardPathSpans; + ContentPathSpans = contentPathSpans; + ItemsSourceSpanStart = itemsSourceSpanStart; + ItemsSourceSpanLength = itemsSourceSpanLength; + } + + public string? TableViewName { get; } + + public string TableViewLine { get; } + + public string? ItemsSourceXBindPath { get; } + + /// + /// Sort member paths extracted from column definitions. + /// + public ImmutableArray SortMemberPaths { get; } + + /// + /// Display member paths extracted from DisplayMemberPath. + /// + public ImmutableArray DisplayMemberPaths { get; } + + /// + /// Cell value paths extracted from CellValuePath. + /// + public ImmutableArray BindingPaths { get; } + + /// + /// Clipboard member paths extracted from ClipboardPath. + /// + public ImmutableArray ClipboardPaths { get; } + + /// + /// Clipboard member paths extracted from ContentPath. + /// + public ImmutableArray ContentPaths { get; } + + /// + /// Parsed column definitions keyed by source line based ColumnId. + /// + public ImmutableArray ColumnDefinitions { get; } + + /// + /// Absolute source spans for . + /// + public ImmutableArray SortMemberPathSpans { get; } + + /// + /// Absolute source spans for . + /// + public ImmutableArray DisplayMemberPathSpans { get; } + + /// + /// Absolute source spans for . + /// + public ImmutableArray BindingPathSpans { get; } + + /// + /// Absolute source spans for . + /// + public ImmutableArray ClipboardPathSpans { get; } + + /// + /// Absolute source spans for . + /// + public ImmutableArray ContentPathSpans { get; } + + public int ItemsSourceSpanStart { get; } + + public int ItemsSourceSpanLength { get; } + } + + /// + /// Parsed TableView data enriched with file and line/column diagnostics metadata. + /// + private readonly struct TableViewXamlInfo + { + public TableViewXamlInfo( + string? tableViewName, + string tableViewLine, + string? itemsSourceXBindPath, + ImmutableArray sortMemberPaths, + ImmutableArray displayMemberPaths, + ImmutableArray cellValuePaths, + ImmutableArray clipboardMemberPaths, + ImmutableArray contentPaths, + ImmutableArray columnDefinitions, + ImmutableArray sortMemberPathLocations, + ImmutableArray displayMemberPathLocations, + ImmutableArray cellValuePathLocations, + ImmutableArray clipboardPathLocations, + ImmutableArray contentPathLocations, + bool hasItemsSourceLocation, + string filePath, + int itemsSourceSpanStart, + int itemsSourceSpanLength, + int itemsSourceStartLine, + int itemsSourceStartColumn, + int itemsSourceEndLine, + int itemsSourceEndColumn) + { + TableViewName = tableViewName; + TableViewLine = tableViewLine; + ItemsSourceXBindPath = itemsSourceXBindPath; + SortMemberPaths = sortMemberPaths; + DisplayMemberPaths = displayMemberPaths; + BindingPaths = cellValuePaths; + ClipboardPaths = clipboardMemberPaths; + ContentPaths = contentPaths; + ColumnDefinitions = columnDefinitions; + SortMemberPathLocations = sortMemberPathLocations; + DisplayMemberPathLocations = displayMemberPathLocations; + BindingPathLocations = cellValuePathLocations; + ClipboardPathLocations = clipboardPathLocations; + ContentPathLocations = contentPathLocations; + HasItemsSourceLocation = hasItemsSourceLocation; + FilePath = filePath; + ItemsSourceSpanStart = itemsSourceSpanStart; + ItemsSourceSpanLength = itemsSourceSpanLength; + ItemsSourceStartLine = itemsSourceStartLine; + ItemsSourceStartColumn = itemsSourceStartColumn; + ItemsSourceEndLine = itemsSourceEndLine; + ItemsSourceEndColumn = itemsSourceEndColumn; + } + + public string? TableViewName { get; } + + public string TableViewLine { get; } + + public string? ItemsSourceXBindPath { get; } + + /// + /// Sort member paths extracted from column definitions. + /// + public ImmutableArray SortMemberPaths { get; } + + /// + /// Display member paths extracted from DisplayMemberPath. + /// + public ImmutableArray DisplayMemberPaths { get; } + + /// + /// Cell value paths extracted from CellValuePath. + /// + public ImmutableArray BindingPaths { get; } + + /// + /// Clipboard member paths extracted from ClipboardPath. + /// + public ImmutableArray ClipboardPaths { get; } + + /// + /// Content member paths extracted from ContentPath. + /// + public ImmutableArray ContentPaths { get; } + + /// + /// Parsed column definitions keyed by source line based ColumnId. + /// + public ImmutableArray ColumnDefinitions { get; } + + /// + /// Diagnostic locations for . + /// + public ImmutableArray SortMemberPathLocations { get; } + + /// + /// Diagnostic locations for . + /// + public ImmutableArray DisplayMemberPathLocations { get; } + + /// + /// Diagnostic locations for . + /// + public ImmutableArray BindingPathLocations { get; } + + /// + /// Diagnostic locations for . + /// + public ImmutableArray ClipboardPathLocations { get; } + + /// + /// Diagnostic locations for . + /// + public ImmutableArray ContentPathLocations { get; } + + public bool HasItemsSourceLocation { get; } + + public string FilePath { get; } + + public int ItemsSourceSpanStart { get; } + + public int ItemsSourceSpanLength { get; } + + public int ItemsSourceStartLine { get; } + + public int ItemsSourceStartColumn { get; } + + public int ItemsSourceEndLine { get; } + + public int ItemsSourceEndColumn { get; } + } + + /// + /// Parsed raw column definition with a stable ColumnId derived from start line. + /// + private readonly struct ColumnDefinitionRaw + { + public ColumnDefinitionRaw( + int startOffset, + int startLine, + BindingEditorKind editorKind, + BindingMode bindingMode, + PathSpanRaw? sortMemberPathSpan, + PathSpanRaw? displayMemberPathSpan, + PathSpanRaw? bindingPathSpan, + PathSpanRaw? clipboardBindingPathSpan, + PathSpanRaw? contentBindingPathSpan) + { + StartOffset = startOffset; + StartLine = startLine; + EditorKind = editorKind; + BindingMode = bindingMode; + SortMemberPathSpan = sortMemberPathSpan; + DisplayMemberPathSpan = displayMemberPathSpan; + BindingPathSpan = bindingPathSpan; + ClipboardPathSpan = clipboardBindingPathSpan; + ContentPathSpan = contentBindingPathSpan; + } + + public int StartOffset { get; } + + public int StartLine { get; } + + public BindingEditorKind EditorKind { get; } + + public BindingMode BindingMode { get; } + + public PathSpanRaw? SortMemberPathSpan { get; } + + public PathSpanRaw? DisplayMemberPathSpan { get; } + + public PathSpanRaw? BindingPathSpan { get; } + + public PathSpanRaw? ClipboardPathSpan { get; } + + public PathSpanRaw? ContentPathSpan { get; } + } + + /// + /// Column definition enriched with file and line/column diagnostics metadata. + /// + private readonly struct ColumnDefinitionInfo + { + public ColumnDefinitionInfo( + int startLine, + BindingEditorKind editorKind, + BindingMode bindingMode, + PathDiagnosticLocation? sortMemberPathLocation, + PathDiagnosticLocation? displayMemberPathLocation, + PathDiagnosticLocation? cellBindingPathLocation, + PathDiagnosticLocation? clipboardBindingPathLocation, + PathDiagnosticLocation? contentBindingPathLocation) + { + StartLine = startLine; + EditorKind = editorKind; + BindingMode = bindingMode; + SortMemberPathLocation = sortMemberPathLocation; + DisplayMemberPathLocation = displayMemberPathLocation; + BindingPathLocation = cellBindingPathLocation; + ClipboardBindingPathLocation = clipboardBindingPathLocation; + ContentBindingPathLocation = contentBindingPathLocation; + } + + public int StartLine { get; } + + public BindingEditorKind EditorKind { get; } + + public BindingMode BindingMode { get; } + + public PathDiagnosticLocation? SortMemberPathLocation { get; } + + public PathDiagnosticLocation? DisplayMemberPathLocation { get; } + + public PathDiagnosticLocation? BindingPathLocation { get; } + + public PathDiagnosticLocation? ClipboardBindingPathLocation { get; } + + public PathDiagnosticLocation? ContentBindingPathLocation { get; } + } + + /// + /// Represents an extracted member path and its absolute span in the source file. + /// + private readonly struct PathSpanRaw + { + public PathSpanRaw(string path, int spanStart, int spanLength) + { + Path = path; + SpanStart = spanStart; + SpanLength = spanLength; + } + + /// + /// The extracted member path text. + /// + public string Path { get; } + + /// + /// The absolute zero-based start index in the source file. + /// + public int SpanStart { get; } + + /// + /// The length of the path span. + /// + public int SpanLength { get; } + } + + /// + /// Stores a member path and the exact source mapping used for diagnostics. + /// + private readonly struct PathDiagnosticLocation + { + public PathDiagnosticLocation( + string path, + string filePath, + int spanStart, + int spanLength, + int startLine, + int startColumn, + int endLine, + int endColumn) + { + Path = path; + FilePath = filePath; + SpanStart = spanStart; + SpanLength = spanLength; + StartLine = startLine; + StartColumn = startColumn; + EndLine = endLine; + EndColumn = endColumn; + } + + /// + /// The extracted member path text. + /// + public string Path { get; } + + /// + /// The source file path containing the path token. + /// + public string FilePath { get; } + + /// + /// The absolute zero-based start index in the source file. + /// + public int SpanStart { get; } + + /// + /// The length of the path span. + /// + public int SpanLength { get; } + + /// + /// Zero-based starting line for the path token. + /// + public int StartLine { get; } + + /// + /// Zero-based starting column for the path token. + /// + public int StartColumn { get; } + + /// + /// Zero-based ending line for the path token. + /// + public int EndLine { get; } + + /// + /// Zero-based ending column for the path token. + /// + public int EndColumn { get; } + } + + /// + /// Code-generation model for one generated provider associated with a TableView. + /// + private readonly struct GeneratedProviderInfo + { + public GeneratedProviderInfo( + string tableViewName, + string providerClassName, + string tableViewLine, + string itemTypeDisplay, + ImmutableArray sortMemberCases, + ImmutableArray displayMemberCases, + ImmutableArray bindingPathCases, + ImmutableArray bindingSetCases, + ImmutableArray clipboardPathCases, + ImmutableArray contentPathCases) + { + TableViewName = tableViewName; + ProviderClassName = providerClassName; + TableViewLine = tableViewLine; + ItemTypeDisplay = itemTypeDisplay; + SortMemberCases = sortMemberCases; + DisplayMemberCases = displayMemberCases; + BindingPathCases = bindingPathCases; + BindingSetCases = bindingSetCases; + ClipboardPathCases = clipboardPathCases; + ContentPathCases = contentPathCases; + } + + public string TableViewName { get; } + + public string ProviderClassName { get; } + + public string TableViewLine { get; } + + public string ItemTypeDisplay { get; } + + /// + /// Generated access cases for sort member paths. + /// + public ImmutableArray SortMemberCases { get; } + + /// + /// Generated access cases for display member paths. + /// + public ImmutableArray DisplayMemberCases { get; } + + /// + /// Generated access cases for cell value paths. + /// + public ImmutableArray BindingPathCases { get; } + + /// + /// Generated set cases for two-way binding paths. + /// + public ImmutableArray BindingSetCases { get; } + + /// + /// Generated access cases for clipboard member paths. + /// + public ImmutableArray ClipboardPathCases { get; } + + /// + /// Generated access cases for clipboard member paths. + /// + public ImmutableArray ContentPathCases { get; } + } + + private readonly struct GeneratedValueCase + { + public GeneratedValueCase(string memberPath, string accessExpression, CaseSourceInfo sourceInfo, string? memberTypeDisplay = null) + { + MemberPath = memberPath; + AccessExpression = accessExpression; + SourceInfo = sourceInfo; + MemberTypeDisplay = memberTypeDisplay; + } + + public string MemberPath { get; } + + public string AccessExpression { get; } + + public CaseSourceInfo SourceInfo { get; } + + public string? MemberTypeDisplay { get; } + } + + private readonly struct GeneratedSetValueCase + { + public GeneratedSetValueCase( + string memberPath, + string localMethodName, + CaseSourceInfo sourceInfo, + ImmutableArray navigationSegments, + string leafMemberName, + string leafValueTypeDisplay, + bool canAssignNull, + bool isLeafNullableValueType, + bool hasNumericConversion, + string numericConversionHelperMethodName, + string numericConvertMethodName, + bool hasDateConversion, + string dateConversionHelperMethodName, + bool hasTimeConversion, + string timeConversionHelperMethodName) + { + MemberPath = memberPath; + LocalMethodName = localMethodName; + SourceInfo = sourceInfo; + NavigationSegments = navigationSegments; + LeafMemberName = leafMemberName; + LeafValueTypeDisplay = leafValueTypeDisplay; + CanAssignNull = canAssignNull; + IsLeafNullableValueType = isLeafNullableValueType; + HasNumericConversion = hasNumericConversion; + NumericConversionHelperMethodName = numericConversionHelperMethodName; + NumericConvertMethodName = numericConvertMethodName; + HasDateConversion = hasDateConversion; + DateConversionHelperMethodName = dateConversionHelperMethodName; + HasTimeConversion = hasTimeConversion; + TimeConversionHelperMethodName = timeConversionHelperMethodName; + } + + public string MemberPath { get; } + + public string LocalMethodName { get; } + + public CaseSourceInfo SourceInfo { get; } + + public ImmutableArray NavigationSegments { get; } + + public string LeafMemberName { get; } + + public string LeafValueTypeDisplay { get; } + + public bool CanAssignNull { get; } + + public bool IsLeafNullableValueType { get; } + + public bool HasNumericConversion { get; } + + public string NumericConversionHelperMethodName { get; } + + public string NumericConvertMethodName { get; } + + public bool HasDateConversion { get; } + + public string DateConversionHelperMethodName { get; } + + public bool HasTimeConversion { get; } + + public string TimeConversionHelperMethodName { get; } + } + + private readonly struct SetNavigationSegment + { + public SetNavigationSegment(string memberName, string memberTypeDisplay) + { + MemberName = memberName; + MemberTypeDisplay = memberTypeDisplay; + } + + public string MemberName { get; } + + public string MemberTypeDisplay { get; } + } + + private readonly struct CaseSourceInfo + { + public static CaseSourceInfo None => new(0, 0); + + public CaseSourceInfo(int columnStartLine, int pathLine) + { + ColumnStartLine = columnStartLine; + PathLine = pathLine; + } + + public int ColumnStartLine { get; } + + public int PathLine { get; } + + public bool HasValue => ColumnStartLine > 0 && PathLine > 0; + } + + private enum BindingEditorKind + { + Unknown, + Other, + Date, + Time + } + + private enum BindingMode + { + OneWay, + OneTime, + TwoWay, + } + + private enum ColumnPathKind + { + SortMemberPath, + DisplayMemberPath, + BindingPath, + ClipboardBindingPath, + ContentBindingPath + } +} + diff --git a/generators/TableViewBindingProviderGenerator.cs b/generators/TableViewBindingProviderGenerator.cs new file mode 100644 index 00000000..236c013d --- /dev/null +++ b/generators/TableViewBindingProviderGenerator.cs @@ -0,0 +1,2595 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; + +namespace WinUI.TableView.SourceGenerators; + +[Generator] +public sealed partial class TableViewBindingProviderGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var xamlFiles = context.AdditionalTextsProvider + .Where(static file => + file.Path.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) + && !IsBuildOutputPath(file.Path)); + + var parsedXamlInfos = xamlFiles + .Select(static (additionalText, cancellationToken) => ParseXamlInfo(additionalText, cancellationToken)) + .Where(static parsed => parsed is not null) + .Select(static (parsed, _) => parsed!.Value) + .Collect(); + + var combined = context.CompilationProvider.Combine(parsedXamlInfos); + + context.RegisterSourceOutput(combined, static (sourceProductionContext, input) => + { + var compilation = input.Left; + var parsedXamls = input.Right; + + var seenClasses = new HashSet(StringComparer.Ordinal); + foreach (var parsed in parsedXamls) + { + if (!seenClasses.Add(parsed.FullyQualifiedClassName)) + { + continue; + } + + var providers = ImmutableArray.CreateBuilder(); + var usedProviderClassNames = new HashSet(StringComparer.Ordinal); + + foreach (var tableView in parsed.TableViews) + { + if (string.IsNullOrWhiteSpace(tableView.TableViewName)) + { + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + TableViewNameRequiredDescriptor, + CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName)); + continue; + } + + var itemType = ResolveItemsSourceItemType( + compilation, + parsed.FullyQualifiedClassName, + tableView.ItemsSourceXBindPath); + + if (itemType is null) continue; + + var itemTypeDisplay = GetGlobalTypeDisplay(itemType); + + if (!string.IsNullOrWhiteSpace(tableView.ItemsSourceXBindPath) + && string.IsNullOrWhiteSpace(itemTypeDisplay)) + { + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveItemsTypeDescriptor, + CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + tableView.ItemsSourceXBindPath)); + } + + if (itemType is null + || string.IsNullOrWhiteSpace(itemTypeDisplay) + || (tableView.SortMemberPaths.Length == 0 + && tableView.ClipboardPaths.Length == 0 + && tableView.DisplayMemberPaths.Length == 0 + && tableView.BindingPaths.Length == 0 + && tableView.ContentPaths.Length == 0)) + { + continue; + } + + var clipboardPathCandidates = ImmutableArray.CreateBuilder(); + foreach (var memberPath in tableView.ClipboardPaths) + { + if (CSharpMemberPathRegex.IsMatch(memberPath)) + { + clipboardPathCandidates.Add(memberPath); + } + } + + var bindingPathCandidates = ImmutableArray.CreateBuilder(); + foreach (var memberPath in tableView.BindingPaths) + { + if (CSharpMemberPathRegex.IsMatch(memberPath)) + { + bindingPathCandidates.Add(memberPath); + } + } + + var contentPathCandidates = ImmutableArray.CreateBuilder(); + foreach (var memberPath in tableView.ContentPaths) + { + if (CSharpMemberPathRegex.IsMatch(memberPath)) + { + contentPathCandidates.Add(memberPath); + } + } + + var displayMemberPathCandidates = ImmutableArray.CreateBuilder(); + foreach (var memberPath in tableView.DisplayMemberPaths) + { + if (CSharpMemberPathRegex.IsMatch(memberPath)) + { + displayMemberPathCandidates.Add(memberPath); + } + } + + var sortMemberPathCandidates = ImmutableArray.CreateBuilder(); + foreach (var memberPath in tableView.SortMemberPaths) + { + if (CSharpMemberPathRegex.IsMatch(memberPath)) + { + sortMemberPathCandidates.Add(memberPath); + } + } + + var sortCases = ImmutableArray.CreateBuilder(); + foreach (var memberPath in sortMemberPathCandidates) + { + if (TryBuildValueAccessExpression(itemType, memberPath, out var accessExpression, out _)) + { + var sourceInfo = GetCaseSourceInfo(tableView.ColumnDefinitions, memberPath, ColumnPathKind.SortMemberPath); + sortCases.Add(new GeneratedValueCase( + memberPath, + accessExpression, + sourceInfo)); + + continue; + } + + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveMemberPathDescriptor, + CreatePathDiagnosticLocation(tableView.SortMemberPathLocations, memberPath) + ?? CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + memberPath, + itemTypeDisplay)); + } + + var clipboardCases = ImmutableArray.CreateBuilder(); + foreach (var memberPath in clipboardPathCandidates) + { + if (TryBuildValueAccessExpression(itemType, memberPath, out var accessExpression, out _)) + { + var sourceInfo = GetCaseSourceInfo(tableView.ColumnDefinitions, memberPath, ColumnPathKind.ClipboardBindingPath); + clipboardCases.Add(new GeneratedValueCase( + memberPath, + accessExpression, + sourceInfo)); + + continue; + } + + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveMemberPathDescriptor, + CreatePathDiagnosticLocation(tableView.ClipboardPathLocations, memberPath) + ?? CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + memberPath, + itemTypeDisplay)); + } + + var bindingPathCases = ImmutableArray.CreateBuilder(); + var bindingSetCases = ImmutableArray.CreateBuilder(); + var usedBindingSetterMethodNames = new HashSet(StringComparer.Ordinal); + foreach (var memberPath in bindingPathCandidates) + { + if (TryBuildValueAccessExpression(itemType, memberPath, out var accessExpression, out var memberType)) + { + var sourceInfo = GetCaseSourceInfo(tableView.ColumnDefinitions, memberPath, ColumnPathKind.BindingPath); + bindingPathCases.Add(new GeneratedValueCase( + memberPath, + accessExpression, + sourceInfo, + GetGlobalTypeDisplay(memberType))); + + if (!IsBindingPathSettable(tableView.ColumnDefinitions, memberPath)) + { + continue; + } + + var editorKind = GetBindingEditorKind(tableView.ColumnDefinitions, sourceInfo.ColumnStartLine); + if (TryBuildSetValueCase(itemType, memberPath, sourceInfo, editorKind, out var setCase)) + { + var localMethodName = GetUniqueIdentifier(setCase.LocalMethodName, usedBindingSetterMethodNames); + bindingSetCases.Add(new GeneratedSetValueCase( + setCase.MemberPath, + localMethodName, + setCase.SourceInfo, + setCase.NavigationSegments, + setCase.LeafMemberName, + setCase.LeafValueTypeDisplay, + setCase.CanAssignNull, + setCase.IsLeafNullableValueType, + setCase.HasNumericConversion, + setCase.NumericConversionHelperMethodName, + setCase.NumericConvertMethodName, + setCase.HasDateConversion, + setCase.DateConversionHelperMethodName, + setCase.HasTimeConversion, + setCase.TimeConversionHelperMethodName)); + } + + continue; + } + + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveMemberPathDescriptor, + CreatePathDiagnosticLocation(tableView.BindingPathLocations, memberPath) + ?? CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + memberPath, + itemTypeDisplay)); + } + + var contentPathCases = ImmutableArray.CreateBuilder(); + foreach (var memberPath in contentPathCandidates) + { + if (TryBuildValueAccessExpression(itemType, memberPath, out var accessExpression, out var memberType)) + { + var sourceInfo = GetCaseSourceInfo(tableView.ColumnDefinitions, memberPath, ColumnPathKind.ContentBindingPath); + contentPathCases.Add(new GeneratedValueCase( + memberPath, + accessExpression, + sourceInfo)); + + continue; + } + + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveMemberPathDescriptor, + CreatePathDiagnosticLocation(tableView.ContentPathLocations, memberPath) + ?? CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + memberPath, + itemTypeDisplay)); + } + + var displayMemberPathCases = ImmutableArray.CreateBuilder(); + foreach (var memberPath in displayMemberPathCandidates) + { + var sourceInfo = GetCaseSourceInfo(tableView.ColumnDefinitions, memberPath, ColumnPathKind.DisplayMemberPath); + var cellValuePathCase = bindingPathCases.FirstOrDefault(c => c.SourceInfo.ColumnStartLine == sourceInfo.ColumnStartLine); + if (string.IsNullOrEmpty(cellValuePathCase.MemberPath)) + { + continue; + } + + if (TryBuildValueAccessExpression(itemType, $"{cellValuePathCase.MemberPath}.{memberPath}", out var accessExpression, out var memberType)) + { + displayMemberPathCases.Add(new GeneratedValueCase( + memberPath, + accessExpression, + sourceInfo)); + + continue; + } + + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + UnableToResolveMemberPathDescriptor, + CreatePathDiagnosticLocation(tableView.DisplayMemberPathLocations, memberPath) + ?? CreateDiagnosticLocation(tableView), + parsed.FullyQualifiedClassName, + memberPath, + itemTypeDisplay)); + } + + if (sortCases.Count == 0 && clipboardCases.Count == 0 && bindingPathCases.Count == 0 && displayMemberPathCases.Count == 0 && contentPathCases.Count == 0) + { + continue; + } + + var baseProviderClassName = $"TableView_{SanitizeIdentifier(tableView.TableViewName!)}_CellValueProvider"; + var providerClassName = GetUniqueIdentifier(baseProviderClassName, usedProviderClassNames); + + providers.Add(new GeneratedProviderInfo( + tableView.TableViewName!, + providerClassName, + tableView.TableViewLine, + itemTypeDisplay!, + sortCases.ToImmutable(), + displayMemberPathCases.ToImmutable(), + bindingPathCases.ToImmutable(), + bindingSetCases.ToImmutable(), + clipboardCases.ToImmutable(), + contentPathCases.ToImmutable())); + } + + if (providers.Count == 0) + { + continue; + } + + if (parsed.IsWindowRoot + && !HasConnectTableViewsCall(compilation, parsed.FullyQualifiedClassName)) + { + sourceProductionContext.ReportDiagnostic( + Diagnostic.Create( + WindowRequiresManualConnectDescriptor, + GetClassLocation(compilation, parsed.FullyQualifiedClassName), + parsed.FullyQualifiedClassName)); + } + + var source = BuildSource(parsed.NamespaceName, parsed.ClassName, providers.ToImmutable()); + sourceProductionContext.AddSource(parsed.HintName, SourceText.From(source, Encoding.UTF8)); + } + }); + } + + private static bool IsBuildOutputPath(string path) + { + var normalizedPath = path.Replace('/', '\\'); + return normalizedPath.IndexOf(@"\bin\", StringComparison.OrdinalIgnoreCase) >= 0 + || normalizedPath.IndexOf(@"\obj\", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static ParsedXamlInfo? ParseXamlInfo( + AdditionalText additionalText, + CancellationToken cancellationToken) + { + var sourceText = additionalText.GetText(cancellationToken); + if (sourceText is null) + { + return null; + } + + var xamlContent = sourceText.ToString(); + if (string.IsNullOrWhiteSpace(xamlContent)) + { + return null; + } + + var isWindowRoot = IsWindowRootElement(xamlContent); + + if (!TryGetWinUITableViews(xamlContent, out var tableViewsRaw) || tableViewsRaw.Length == 0) + { + return null; + } + + var classMatch = XamlClassRegex.Match(xamlContent); + if (!classMatch.Success) + { + return null; + } + + var fullyQualifiedClassName = classMatch.Groups["className"].Value.Trim(); + if (string.IsNullOrWhiteSpace(fullyQualifiedClassName)) + { + return null; + } + + var (namespaceName, className) = SplitClassName(fullyQualifiedClassName); + if (string.IsNullOrWhiteSpace(className)) + { + return null; + } + + var tableViews = ImmutableArray.CreateBuilder(); + foreach (var tableViewRaw in tableViewsRaw) + { + var hasItemsSourceLocation = tableViewRaw.ItemsSourceSpanStart >= 0 + && tableViewRaw.ItemsSourceSpanLength > 0 + && sourceText.Lines.Count > 0; + + var startLine = 0; + var startColumn = 0; + var endLine = 0; + var endColumn = 0; + + if (hasItemsSourceLocation) + { + var span = new TextSpan(tableViewRaw.ItemsSourceSpanStart, tableViewRaw.ItemsSourceSpanLength); + var lineSpan = sourceText.Lines.GetLinePositionSpan(span); + startLine = lineSpan.Start.Line; + startColumn = lineSpan.Start.Character; + endLine = lineSpan.End.Line; + endColumn = lineSpan.End.Character; + } + + tableViews.Add( + new TableViewXamlInfo( + tableViewRaw.TableViewName, + tableViewRaw.TableViewLine, + tableViewRaw.ItemsSourceXBindPath, + tableViewRaw.SortMemberPaths, + tableViewRaw.DisplayMemberPaths, + tableViewRaw.BindingPaths, + tableViewRaw.ClipboardPaths, + tableViewRaw.ContentPaths, + CreateColumnDefinitions(sourceText, additionalText.Path, tableViewRaw.ColumnDefinitions), + CreatePathDiagnosticLocations(sourceText, additionalText.Path, tableViewRaw.SortMemberPathSpans), + CreatePathDiagnosticLocations(sourceText, additionalText.Path, tableViewRaw.DisplayMemberPathSpans), + CreatePathDiagnosticLocations(sourceText, additionalText.Path, tableViewRaw.BindingPathSpans), + CreatePathDiagnosticLocations(sourceText, additionalText.Path, tableViewRaw.ClipboardPathSpans), + CreatePathDiagnosticLocations(sourceText, additionalText.Path, tableViewRaw.ContentPathSpans), + hasItemsSourceLocation, + additionalText.Path, + tableViewRaw.ItemsSourceSpanStart, + tableViewRaw.ItemsSourceSpanLength, + startLine, + startColumn, + endLine, + endColumn)); + } + + var hintName = $"{fullyQualifiedClassName.Replace('.', '_')}.TableViewUsage.g.cs"; + return new ParsedXamlInfo( + namespaceName, + className, + fullyQualifiedClassName, + hintName, + isWindowRoot, + tableViews.ToImmutable()); + } + + private static ImmutableArray CreateColumnDefinitions( + SourceText sourceText, + string filePath, + ImmutableArray columnDefinitions) + { + if (columnDefinitions.Length == 0) + { + return []; + } + + var builder = ImmutableArray.CreateBuilder(columnDefinitions.Length); + foreach (var column in columnDefinitions) + { + builder.Add( + new ColumnDefinitionInfo( + column.StartLine, + column.EditorKind, + column.BindingMode, + CreatePathDiagnosticLocation(sourceText, filePath, column.SortMemberPathSpan), + CreatePathDiagnosticLocation(sourceText, filePath, column.DisplayMemberPathSpan), + CreatePathDiagnosticLocation(sourceText, filePath, column.BindingPathSpan), + CreatePathDiagnosticLocation(sourceText, filePath, column.ClipboardPathSpan), + CreatePathDiagnosticLocation(sourceText, filePath, column.ContentPathSpan))); + } + + return builder.ToImmutable(); + } + + /// + /// Converts raw absolute path spans into Roslyn-friendly file and line mapped locations. + /// + /// The full XAML source text. + /// The source file path for the XAML file. + /// Absolute spans for extracted member paths. + /// A list of member-path diagnostic locations. + private static ImmutableArray CreatePathDiagnosticLocations( + SourceText sourceText, + string filePath, + ImmutableArray pathSpans) + { + if (pathSpans.Length == 0 || sourceText.Lines.Count == 0) + { + return []; + } + + var builder = ImmutableArray.CreateBuilder(pathSpans.Length); + foreach (var pathSpan in pathSpans) + { + if (pathSpan.SpanStart < 0 || pathSpan.SpanLength <= 0) + { + continue; + } + + var span = new TextSpan(pathSpan.SpanStart, pathSpan.SpanLength); + var lineSpan = sourceText.Lines.GetLinePositionSpan(span); + builder.Add(new PathDiagnosticLocation( + pathSpan.Path, + filePath, + pathSpan.SpanStart, + pathSpan.SpanLength, + lineSpan.Start.Line, + lineSpan.Start.Character, + lineSpan.End.Line, + lineSpan.End.Character)); + } + + return builder.ToImmutable(); + } + + private static bool IsWindowRootElement(string xamlContent) + { + var index = 0; + while (index < xamlContent.Length) + { + var ltIndex = xamlContent.IndexOf('<', index); + if (ltIndex < 0 || ltIndex + 1 >= xamlContent.Length) + { + return false; + } + + var nextChar = xamlContent[ltIndex + 1]; + + if (nextChar == '?') + { + var piEnd = xamlContent.IndexOf("?>", ltIndex + 2, StringComparison.Ordinal); + if (piEnd < 0) + { + return false; + } + + index = piEnd + 2; + continue; + } + + if (nextChar == '!') + { + if (xamlContent.AsSpan(ltIndex).StartsWith("", ltIndex + 4, StringComparison.Ordinal); + if (commentEnd < 0) + { + return false; + } + + index = commentEnd + 3; + continue; + } + + var declarationEnd = xamlContent.IndexOf('>', ltIndex + 2); + if (declarationEnd < 0) + { + return false; + } + + index = declarationEnd + 1; + continue; + } + + var nameStart = ltIndex + 1; + while (nameStart < xamlContent.Length && char.IsWhiteSpace(xamlContent[nameStart])) + { + nameStart++; + } + + var nameEnd = nameStart; + while (nameEnd < xamlContent.Length + && !char.IsWhiteSpace(xamlContent[nameEnd]) + && xamlContent[nameEnd] is not '>' and not '/') + { + nameEnd++; + } + + if (nameEnd <= nameStart) + { + return false; + } + + var qualifiedName = xamlContent.Substring(nameStart, nameEnd - nameStart); + var colonIndex = qualifiedName.IndexOf(':'); + var localName = colonIndex >= 0 ? qualifiedName.Substring(colonIndex + 1) : qualifiedName; + + return localName.Equals("Window", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private static bool TryGetWinUITableViews(string xamlContent, out ImmutableArray tableViews) + { + var commentRanges = GetCommentRanges(xamlContent); + var allowedPrefixes = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (Match match in XmlnsPrefixRegex.Matches(xamlContent)) + { + var prefix = match.Groups["prefix"].Value.Trim(); + if (!string.IsNullOrWhiteSpace(prefix)) + { + allowedPrefixes.Add(prefix); + } + } + + var defaultNamespaceIsWinUITableView = XmlnsDefaultRegex.IsMatch(xamlContent); + var builder = ImmutableArray.CreateBuilder(); + + for (var match = TableViewTagRegex.Match(xamlContent); match.Success; match = match.NextMatch()) + { + if (IsIndexInAnyRange(match.Index, commentRanges)) + { + continue; + } + + var prefixGroup = match.Groups["prefix"]; + var isValidTag = prefixGroup.Success + ? allowedPrefixes.Contains(prefixGroup.Value) + : defaultNamespaceIsWinUITableView; + + if (!isValidTag) + { + continue; + } + + var startTagText = ExtractStartTagText(xamlContent, match.Index); + if (string.IsNullOrWhiteSpace(startTagText)) + { + continue; + } + + var tableViewLine = GetLineAtIndex(xamlContent, match.Index); + if (string.IsNullOrWhiteSpace(tableViewLine)) + { + continue; + } + + var tableViewSegment = ExtractTableViewSegment(xamlContent, match.Index, startTagText, prefixGroup); + var tableViewName = TryExtractTableViewName(startTagText); + var itemsSourceXBindPath = TryExtractItemsSourceXBindPath( + startTagText, + match.Index, + out var itemsSourceSpanInText); + + var columnDefinitions = ExtractColumnDefinitions(xamlContent, tableViewSegment, match.Index); + var sortMemberPathSpans = GetPathSpans(columnDefinitions, static column => column.SortMemberPathSpan); + var displayMemberPathSpans = GetPathSpans(columnDefinitions, static column => column.DisplayMemberPathSpan); + var bingingPathSpans = GetPathSpans(columnDefinitions, static column => column.BindingPathSpan); + var clipboardPathSpans = GetPathSpans(columnDefinitions, static column => column.ClipboardPathSpan); + var contentPathSpans = GetPathSpans(columnDefinitions, static column => column.ContentPathSpan); + var sortMemberPaths = ExtractPaths(sortMemberPathSpans); + var displayMemberPaths = ExtractPaths(displayMemberPathSpans); + var bindingPaths = ExtractPaths(bingingPathSpans); + var clipboardPaths = ExtractPaths(clipboardPathSpans); + var contentPaths = ExtractPaths(contentPathSpans); + + builder.Add(new TableViewInfoRaw( + tableViewName, + tableViewLine, + itemsSourceXBindPath, + sortMemberPaths, + displayMemberPaths, + bindingPaths, + clipboardPaths, + contentPaths, + columnDefinitions, + sortMemberPathSpans, + displayMemberPathSpans, + bingingPathSpans, + clipboardPathSpans, + contentPathSpans, + itemsSourceSpanInText.Start, + itemsSourceSpanInText.Length)); + } + + tableViews = builder.ToImmutable(); + return tableViews.Length > 0; + } + + private static ImmutableArray<(int Start, int End)> GetCommentRanges(string xamlContent) + { + var ranges = ImmutableArray.CreateBuilder<(int Start, int End)>(); + var searchIndex = 0; + + while (searchIndex < xamlContent.Length) + { + var commentStart = xamlContent.IndexOf("", commentStart + 4, StringComparison.Ordinal); + var commentEnd = commentEndMarker < 0 ? xamlContent.Length : commentEndMarker + 3; + ranges.Add((commentStart, commentEnd)); + + searchIndex = commentEnd; + } + + return ranges.ToImmutable(); + } + + private static bool IsIndexInAnyRange(int index, ImmutableArray<(int Start, int End)> ranges) + { + foreach (var (Start, End) in ranges) + { + if (index >= Start && index < End) + { + return true; + } + } + + return false; + } + + private static string? TryExtractTableViewName(string startTagText) + { + var match = TableViewNameRegex.Match(startTagText); + if (!match.Success) + { + return null; + } + + var name = match.Groups["name"].Value.Trim(); + return string.IsNullOrWhiteSpace(name) ? null : name; + } + + private static ImmutableArray ExtractColumnDefinitions( + string fullXamlContent, + string tableViewSegment, + int tableViewSegmentStart) + { + if (string.IsNullOrWhiteSpace(tableViewSegment)) + { + return []; + } + + var commentRanges = GetCommentRanges(tableViewSegment); + var builder = ImmutableArray.CreateBuilder(); + foreach (Match columnMatch in ColumnTagRegex.Matches(tableViewSegment)) + { + if (IsIndexInAnyRange(columnMatch.Index, commentRanges)) + { + continue; + } + + var attrsGroup = columnMatch.Groups["attrs"]; + var attrsText = attrsGroup.Success ? attrsGroup.Value : string.Empty; + var columnTypeText = columnMatch.Groups["columnType"].Success + ? columnMatch.Groups["columnType"].Value + : string.Empty; + var attrsStart = tableViewSegmentStart + attrsGroup.Index; + var absoluteColumnStart = tableViewSegmentStart + columnMatch.Index; + var columnStartLine = GetLineNumber(fullXamlContent, absoluteColumnStart); + var editorKind = GetBindingEditorKind(columnTypeText); + + var sortMemberPathSpan = TryExtractPathSpan(attrsText, attrsStart, SortMemberPathRegex); + var displayMemberPathSpan = TryExtractPathSpan(attrsText, attrsStart, DisplayMemberPathRegex); + var bindingExtraction = TryExtractBindingPathSpanWithMode(attrsText, attrsStart, CellBindingRegex); + var bindingPathSpan = bindingExtraction.PathSpan; + var clipboardBindingPathSpan = TryExtractBindingPathSpan(attrsText, attrsStart, ClipboardBindingRegex); + var contentBindingPathSpan = TryExtractBindingPathSpan(attrsText, attrsStart, ContentBindingRegex); + + builder.Add(new ColumnDefinitionRaw( + absoluteColumnStart, + columnStartLine, + editorKind, + bindingExtraction.BindingMode, + sortMemberPathSpan, + displayMemberPathSpan, + bindingPathSpan, + clipboardBindingPathSpan, + contentBindingPathSpan)); + } + + return builder.ToImmutable(); + } + + private static PathDiagnosticLocation? CreatePathDiagnosticLocation( + SourceText sourceText, + string filePath, + PathSpanRaw? pathSpan) + { + if (!pathSpan.HasValue || sourceText.Lines.Count == 0) + { + return null; + } + + var value = pathSpan.Value; + if (value.SpanStart < 0 || value.SpanLength <= 0) + { + return null; + } + + var span = new TextSpan(value.SpanStart, value.SpanLength); + var lineSpan = sourceText.Lines.GetLinePositionSpan(span); + return new PathDiagnosticLocation( + value.Path, + filePath, + value.SpanStart, + value.SpanLength, + lineSpan.Start.Line, + lineSpan.Start.Character, + lineSpan.End.Line, + lineSpan.End.Character); + } + + private static int GetLineNumber(string content, int absoluteIndex) + { + if (absoluteIndex <= 0) + { + return 1; + } + + var line = 1; + var max = Math.Min(absoluteIndex, content.Length); + for (var i = 0; i < max; i++) + { + if (content[i] == '\n') + { + line++; + } + } + + return line; + } + + private static PathSpanRaw? TryExtractPathSpan(string attrsText, int attrsStart, Regex regex) + { + if (string.IsNullOrWhiteSpace(attrsText)) + { + return null; + } + + var match = regex.Match(attrsText); + if (!match.Success) + { + return null; + } + + var pathGroup = match.Groups["path"]; + var path = pathGroup.Value.Trim(); + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + return new PathSpanRaw(path, attrsStart + pathGroup.Index, Math.Max(1, pathGroup.Length)); + } + + private static PathSpanRaw? TryExtractBindingPathSpan(string attrsText, int attrsStart, Regex bindingRegex) + { + return TryExtractBindingPathSpanWithMode(attrsText, attrsStart, bindingRegex).PathSpan; + } + + private static (PathSpanRaw? PathSpan, BindingMode BindingMode) TryExtractBindingPathSpanWithMode(string attrsText, int attrsStart, Regex bindingRegex) + { + if (string.IsNullOrWhiteSpace(attrsText)) + { + return (null, BindingMode.TwoWay); + } + + var bindingMatch = bindingRegex.Match(attrsText); + if (!bindingMatch.Success) + { + return (null, BindingMode.TwoWay); + } + + var bodyGroup = bindingMatch.Groups["body"]; + var bodyText = bodyGroup.Value; + var path = TryExtractBindingPath(bodyText); + if (string.IsNullOrWhiteSpace(path)) + { + return (null, BindingMode.TwoWay); + } + + var bindingMode = TryExtractBindingMode(bodyText); + var body = bodyText; + var pathOffsetInBody = body.IndexOf(path, StringComparison.Ordinal); + if (pathOffsetInBody < 0) + { + pathOffsetInBody = 0; + } + + var resolvedPath = path!; + var pathStart = attrsStart + bodyGroup.Index + pathOffsetInBody; + return (new PathSpanRaw(resolvedPath, pathStart, Math.Max(1, resolvedPath.Length)), bindingMode); + } + + private static BindingMode TryExtractBindingMode(string bindingBody) + { + if (string.IsNullOrWhiteSpace(bindingBody)) + { + return BindingMode.TwoWay; + } + + var modeMatch = BindingModeTokenRegex.Match(bindingBody.Trim()); + if (!modeMatch.Success) + { + return BindingMode.TwoWay; + } + + var modeText = modeMatch.Groups["mode"].Value.Trim(); + modeText = modeText.Trim('\"', '\''); + if (modeText.Equals("TwoWay", StringComparison.OrdinalIgnoreCase)) + { + return BindingMode.TwoWay; + } + + if (modeText.Equals("OneWay", StringComparison.OrdinalIgnoreCase)) + { + return BindingMode.OneWay; + } + + if (modeText.Equals("OneTime", StringComparison.OrdinalIgnoreCase)) + { + return BindingMode.OneTime; + } + + return BindingMode.TwoWay; + } + + private static string? TryExtractBindingPath(string bindingBody) + { + if (string.IsNullOrWhiteSpace(bindingBody)) + { + return null; + } + + var trimmedBody = bindingBody.Trim(); + if (NonSimpleBindingTokenRegex.IsMatch(trimmedBody)) + { + return null; + } + + // Handles: {Binding Path=Id} and other bodies containing Path=... + var explicitPathMatch = BindingPathTokenRegex.Match(trimmedBody); + if (explicitPathMatch.Success) + { + var explicitPath = explicitPathMatch.Groups["path"].Value.Trim(); + if (!string.IsNullOrWhiteSpace(explicitPath)) + { + return explicitPath; + } + } + + // Handles: {Binding Id} + var firstSegment = trimmedBody.Split(',')[0].Trim(); + if (string.IsNullOrWhiteSpace(firstSegment) || firstSegment.Contains('=')) + { + return null; + } + + return firstSegment; + } + + private static ImmutableArray GetPathSpans( + ImmutableArray columns, + Func selector) + { + if (columns.Length == 0) + { + return []; + } + + var uniquePaths = new HashSet(StringComparer.Ordinal); + var builder = ImmutableArray.CreateBuilder(); + foreach (var column in columns) + { + var pathSpan = selector(column); + if (!pathSpan.HasValue || !uniquePaths.Add(pathSpan.Value.Path)) + { + continue; + } + + builder.Add(pathSpan.Value); + } + + return builder.ToImmutable(); + } + + /// + /// Projects path span entries to their path text in declaration order. + /// + /// Path span entries. + /// Ordered unique path texts. + private static ImmutableArray ExtractPaths(ImmutableArray pathSpans) + { + if (pathSpans.Length == 0) + { + return []; + } + + var builder = ImmutableArray.CreateBuilder(pathSpans.Length); + foreach (var pathSpan in pathSpans) + { + builder.Add(pathSpan.Path); + } + + return builder.ToImmutable(); + } + + private static string ExtractTableViewSegment( + string xamlContent, + int tableViewTagStartIndex, + string tableViewStartTagText, + Group prefixGroup) + { + if (tableViewStartTagText.EndsWith("/>", StringComparison.Ordinal)) + { + return tableViewStartTagText; + } + + var tableViewTagName = prefixGroup.Success + ? $"{prefixGroup.Value}:TableView" + : "TableView"; + + var closingTag = $""; + var contentStart = tableViewTagStartIndex + tableViewStartTagText.Length; + if (contentStart >= xamlContent.Length) + { + return tableViewStartTagText; + } + + var closingIndex = xamlContent.IndexOf(closingTag, contentStart, StringComparison.OrdinalIgnoreCase); + if (closingIndex < 0) + { + return tableViewStartTagText; + } + + var totalLength = closingIndex + closingTag.Length - tableViewTagStartIndex; + if (totalLength <= 0 || tableViewTagStartIndex + totalLength > xamlContent.Length) + { + return tableViewStartTagText; + } + + return xamlContent.Substring(tableViewTagStartIndex, totalLength); + } + + private static string ExtractStartTagText(string xamlContent, int tagStartIndex) + { + if (tagStartIndex < 0 || tagStartIndex >= xamlContent.Length) + { + return string.Empty; + } + + var endIndex = xamlContent.IndexOf('>', tagStartIndex); + if (endIndex < 0) + { + return string.Empty; + } + + return xamlContent.Substring(tagStartIndex, endIndex - tagStartIndex + 1); + } + + private static string? TryExtractItemsSourceXBindPath( + string startTagText, + int startTagGlobalIndex, + out (int Start, int Length) itemsSourceSpanInText) + { + var bindingMatch = ItemsSourceBindingRegex.Match(startTagText); + if (!bindingMatch.Success) + { + itemsSourceSpanInText = (-1, 0); + return null; + } + + itemsSourceSpanInText = ( + startTagGlobalIndex + bindingMatch.Index, + Math.Max(1, "ItemsSource".Length)); + + var kind = bindingMatch.Groups["kind"].Value.Trim(); + if (!kind.Equals("x:Bind", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var body = bindingMatch.Groups["body"].Value.Trim(); + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + var firstSegment = body.Split(',')[0].Trim(); + if (string.IsNullOrWhiteSpace(firstSegment)) + { + return null; + } + + if (firstSegment.StartsWith("Path=", StringComparison.OrdinalIgnoreCase)) + { + firstSegment = firstSegment.Substring("Path=".Length).Trim(); + } + + if (string.IsNullOrWhiteSpace(firstSegment) || firstSegment.Contains('=')) + { + return null; + } + + return firstSegment; + } + + private static Location? CreateDiagnosticLocation(TableViewXamlInfo tableView) + { + if (!tableView.HasItemsSourceLocation + || string.IsNullOrWhiteSpace(tableView.FilePath) + || tableView.ItemsSourceSpanStart < 0 + || tableView.ItemsSourceSpanLength <= 0) + { + return null; + } + + var textSpan = new TextSpan(tableView.ItemsSourceSpanStart, tableView.ItemsSourceSpanLength); + var lineSpan = new LinePositionSpan( + new LinePosition(tableView.ItemsSourceStartLine, tableView.ItemsSourceStartColumn), + new LinePosition(tableView.ItemsSourceEndLine, tableView.ItemsSourceEndColumn)); + + return Location.Create(tableView.FilePath, textSpan, lineSpan); + } + + /// + /// Finds the diagnostic location for the specified member path. + /// + /// Known locations keyed by member path text. + /// The member path to locate. + /// The matching source location, or when not found. + private static Location? CreatePathDiagnosticLocation( + ImmutableArray pathLocations, + string path) + { + foreach (var pathLocation in pathLocations) + { + if (!pathLocation.Path.Equals(path, StringComparison.Ordinal)) + { + continue; + } + + var textSpan = new TextSpan(pathLocation.SpanStart, pathLocation.SpanLength); + var lineSpan = new LinePositionSpan( + new LinePosition(pathLocation.StartLine, pathLocation.StartColumn), + new LinePosition(pathLocation.EndLine, pathLocation.EndColumn)); + + return Location.Create(pathLocation.FilePath, textSpan, lineSpan); + } + + return null; + } + + private static CaseSourceInfo GetCaseSourceInfo( + ImmutableArray columns, + string memberPath, + ColumnPathKind kind) + { + foreach (var column in columns) + { + var location = kind switch + { + ColumnPathKind.SortMemberPath => column.SortMemberPathLocation, + ColumnPathKind.DisplayMemberPath => column.DisplayMemberPathLocation, + ColumnPathKind.BindingPath => column.BindingPathLocation, + ColumnPathKind.ClipboardBindingPath => column.ClipboardBindingPathLocation, + ColumnPathKind.ContentBindingPath => column.ContentBindingPathLocation, + _ => null + }; + + if (!location.HasValue || !location.Value.Path.Equals(memberPath, StringComparison.Ordinal)) + { + continue; + } + + return new CaseSourceInfo( + column.StartLine, + location.Value.StartLine + 1); + } + + return CaseSourceInfo.None; + } + + private static BindingEditorKind GetBindingEditorKind(string columnTypeName) + { + if (columnTypeName.Equals("TableViewDateColumn", StringComparison.OrdinalIgnoreCase)) + { + return BindingEditorKind.Date; + } + + if (columnTypeName.Equals("TableViewTimeColumn", StringComparison.OrdinalIgnoreCase)) + { + return BindingEditorKind.Time; + } + + return BindingEditorKind.Other; + } + + private static BindingEditorKind GetBindingEditorKind(ImmutableArray columns, int columnStartLine) + { + if (columnStartLine <= 0) + { + return BindingEditorKind.Unknown; + } + + foreach (var column in columns) + { + if (column.StartLine == columnStartLine) + { + return column.EditorKind; + } + } + + return BindingEditorKind.Unknown; + } + + private static bool IsBindingPathSettable(ImmutableArray columns, string memberPath) + { + foreach (var column in columns) + { + if (column.BindingPathLocation.HasValue + && column.BindingPathLocation.Value.Path.Equals(memberPath, StringComparison.Ordinal) + && column.BindingMode == BindingMode.TwoWay) + { + return true; + } + } + + return false; + } + + private static Location? GetClassLocation(Compilation compilation, string fullyQualifiedClassName) + { + var type = compilation.GetTypeByMetadataName(fullyQualifiedClassName); + if (type is null) + { + return null; + } + + foreach (var location in type.Locations) + { + if (location.IsInSource) + { + return location; + } + } + + return null; + } + + private static bool HasConnectTableViewsCall(Compilation compilation, string fullyQualifiedClassName) + { + var type = compilation.GetTypeByMetadataName(fullyQualifiedClassName); + if (type is null) + { + return false; + } + + foreach (var declaration in type.DeclaringSyntaxReferences) + { + var syntax = declaration.GetSyntax(); + if (syntax is not TypeDeclarationSyntax typeDeclaration) + { + continue; + } + + foreach (var node in typeDeclaration.DescendantNodes()) + { + if (node is not InvocationExpressionSyntax invocation) + { + continue; + } + + if (invocation.Expression is IdentifierNameSyntax identifier + && identifier.Identifier.ValueText.Equals("ConnectTableViews", StringComparison.Ordinal)) + { + return true; + } + + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess + && memberAccess.Name.Identifier.ValueText.Equals("ConnectTableViews", StringComparison.Ordinal)) + { + return true; + } + } + } + + return false; + } + + private static ITypeSymbol? ResolveItemsSourceItemType( + Compilation compilation, + string fullyQualifiedClassName, + string? xBindPath) + { + if (xBindPath is null) + { + return null; + } + + var pageType = compilation.GetTypeByMetadataName(fullyQualifiedClassName); + if (pageType is null) + { + return null; + } + + var bindingPath = xBindPath.Trim(); + if (bindingPath.Length == 0) + { + return null; + } + + if (bindingPath.StartsWith("this.", StringComparison.Ordinal)) + { + bindingPath = bindingPath.Substring("this.".Length); + } + + if (string.IsNullOrWhiteSpace(bindingPath)) + { + return null; + } + + ITypeSymbol currentType = pageType; + var segments = bindingPath.Split('.'); + foreach (var rawSegment in segments) + { + var segment = rawSegment.Trim().TrimEnd('!', '?'); + if (string.IsNullOrWhiteSpace(segment) + || segment.Contains('(') + || segment.Contains('[') + || segment.Contains(']')) + { + return null; + } + + if (!TryGetInstanceMemberType(currentType, segment, out var memberType)) + { + return null; + } + + currentType = memberType; + } + + if (!TryGetCollectionItemType(currentType, out var itemType)) + { + return null; + } + + return itemType; + } + + private static bool TryGetInstanceMemberType(ITypeSymbol type, string memberName, out ITypeSymbol memberType) + { + if (TryGetInstanceMember(type, memberName, out _, out memberType)) + { + return true; + } + + memberType = null!; + return false; + } + + private static bool TryGetInstanceMember(ITypeSymbol type, string memberName, out ISymbol memberSymbol, out ITypeSymbol memberType) + { + for (var current = type; current is not null; current = current.BaseType) + { + foreach (var member in current.GetMembers(memberName)) + { + if (member.IsStatic) + { + continue; + } + + switch (member) + { + case IPropertySymbol property: + memberSymbol = property; + memberType = property.Type; + return true; + case IFieldSymbol field: + memberSymbol = field; + memberType = field.Type; + return true; + } + } + } + + memberSymbol = null!; + memberType = null!; + return false; + } + + private static bool TryBuildSetValueCase( + ITypeSymbol itemType, + string memberPath, + CaseSourceInfo sourceInfo, + BindingEditorKind editorKind, + out GeneratedSetValueCase setValueCase) + { + setValueCase = default; + if (string.IsNullOrWhiteSpace(memberPath)) + { + return false; + } + + var rawSegments = memberPath.Split('.'); + if (rawSegments.Length == 0) + { + return false; + } + + var segments = new string[rawSegments.Length]; + for (var i = 0; i < rawSegments.Length; i++) + { + var segment = rawSegments[i].Trim(); + if (string.IsNullOrWhiteSpace(segment)) + { + return false; + } + + segments[i] = segment; + } + + var currentType = itemType; + var navBuilder = ImmutableArray.CreateBuilder(); + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + if (!TryGetInstanceMember(currentType, segment, out var memberSymbol, out var memberType)) + { + return false; + } + + var unwrappedType = UnwrapNullable(memberType); + var memberTypeDisplay = GetGlobalTypeDisplay(unwrappedType); + + if (i == segments.Length - 1) + { + if (!IsWritableMember(memberSymbol)) + { + return false; + } + + var leafType = memberType; + var leafUnderlyingType = UnwrapNullable(leafType); + var hasNumericConversion = TryGetNumericConversionInfo(leafUnderlyingType, out var numericConversionHelperName, out var numericConvertMethodName); + var hasDateConversion = TryGetDateConversionInfo(editorKind, leafUnderlyingType, out var dateConversionHelperMethodName); + var hasTimeConversion = TryGetTimeConversionInfo(editorKind, leafUnderlyingType, out var timeConversionHelperMethodName); + setValueCase = new GeneratedSetValueCase( + memberPath, + localMethodName: "TrySet_" + SanitizeIdentifier(memberPath.Replace('.', '_')), + sourceInfo, + navBuilder.ToImmutable(), + segment, + GetGlobalTypeDisplay(leafUnderlyingType), + CanBeNull(leafType), + IsNullableValueType(leafType), + hasNumericConversion, + numericConversionHelperName, + numericConvertMethodName, + hasDateConversion, + dateConversionHelperMethodName, + hasTimeConversion, + timeConversionHelperMethodName); + return true; + } + + navBuilder.Add(new SetNavigationSegment(segment, memberTypeDisplay)); + currentType = unwrappedType; + } + + return false; + } + + private static bool IsWritableMember(ISymbol memberSymbol) + { + return memberSymbol switch + { + IPropertySymbol property => property.SetMethod is not null && !property.SetMethod.IsStatic, + IFieldSymbol field => !field.IsReadOnly && !field.IsConst && !field.IsStatic, + _ => false + }; + } + + private static bool TryGetNumericConversionInfo(ITypeSymbol type, out string helperMethodName, out string convertMethodName) + { + helperMethodName = string.Empty; + convertMethodName = string.Empty; + + switch (type.SpecialType) + { + case SpecialType.System_Byte: + helperMethodName = "TryGetByteFromDouble"; + convertMethodName = "ToByte"; + return true; + case SpecialType.System_SByte: + helperMethodName = "TryGetSByteFromDouble"; + convertMethodName = "ToSByte"; + return true; + case SpecialType.System_Int16: + helperMethodName = "TryGetInt16FromDouble"; + convertMethodName = "ToInt16"; + return true; + case SpecialType.System_UInt16: + helperMethodName = "TryGetUInt16FromDouble"; + convertMethodName = "ToUInt16"; + return true; + case SpecialType.System_Int32: + helperMethodName = "TryGetInt32FromDouble"; + convertMethodName = "ToInt32"; + return true; + case SpecialType.System_UInt32: + helperMethodName = "TryGetUInt32FromDouble"; + convertMethodName = "ToUInt32"; + return true; + case SpecialType.System_Int64: + helperMethodName = "TryGetInt64FromDouble"; + convertMethodName = "ToInt64"; + return true; + case SpecialType.System_UInt64: + helperMethodName = "TryGetUInt64FromDouble"; + convertMethodName = "ToUInt64"; + return true; + case SpecialType.System_Single: + helperMethodName = "TryGetSingleFromDouble"; + convertMethodName = "ToSingle"; + return true; + case SpecialType.System_Double: + helperMethodName = "TryGetDoubleFromDouble"; + convertMethodName = "ToDouble"; + return true; + case SpecialType.System_Decimal: + helperMethodName = "TryGetDecimalFromDouble"; + convertMethodName = "ToDecimal"; + return true; + default: + return false; + } + } + + private static bool TryGetDateConversionInfo(BindingEditorKind editorKind, ITypeSymbol type, out string helperMethodName) + { + helperMethodName = string.Empty; + if (editorKind != BindingEditorKind.Date) + { + return false; + } + + if (type.SpecialType == SpecialType.System_DateTime) + { + helperMethodName = "TryGetDateTimeFromDateTimeOffset"; + return true; + } + + if (type is INamedTypeSymbol namedType + && namedType.ContainingNamespace?.ToDisplayString() == "System") + { + if (namedType.Name == "DateOnly") + { + helperMethodName = "TryGetDateOnlyFromDateTimeOffset"; + return true; + } + + if (namedType.Name == "DateTimeOffset") + { + helperMethodName = "TryGetDateTimeOffsetFromDateTimeOffset"; + return true; + } + } + + return false; + } + + private static bool TryGetTimeConversionInfo(BindingEditorKind editorKind, ITypeSymbol type, out string helperMethodName) + { + helperMethodName = string.Empty; + if (editorKind != BindingEditorKind.Time) + { + return false; + } + + if (type is INamedTypeSymbol namedType + && namedType.ContainingNamespace?.ToDisplayString() == "System") + { + if (namedType.Name == "TimeSpan") + { + helperMethodName = "TryGetTimeSpanFromTimeSpan"; + return true; + } + + if (namedType.Name == "TimeOnly") + { + helperMethodName = "TryGetTimeOnlyFromTimeSpan"; + return true; + } + + if (namedType.Name == "DateTime") + { + helperMethodName = "TryGetDateTimeFromTimeSpan"; + return true; + } + + if (namedType.Name == "DateTimeOffset") + { + helperMethodName = "TryGetDateTimeOffsetFromTimeSpan"; + return true; + } + } + + return false; + } + + private static bool TryBuildValueAccessExpression( + ITypeSymbol itemType, + string memberPath, + out string accessExpression, + out ITypeSymbol memberType) + { + accessExpression = string.Empty; + memberType = null!; + if (string.IsNullOrWhiteSpace(memberPath)) + { + return false; + } + + var segments = memberPath.Split('.'); + if (segments.Length == 0) + { + return false; + } + + var currentType = itemType; + var builder = new StringBuilder(); + + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i].Trim(); + if (string.IsNullOrWhiteSpace(segment)) + { + return false; + } + + if (!TryGetInstanceMemberType(currentType, segment, out memberType)) + { + return false; + } + + builder.Append('.').Append(segment); + if (i < segments.Length - 1 && CanBeNull(memberType)) + { + builder.Append('?'); + } + + currentType = UnwrapNullable(memberType); + } + + memberType = currentType; + accessExpression = builder.ToString(); + return true; + } + + private static bool CanBeNull(ITypeSymbol type) + { + if (type.IsReferenceType) + { + return true; + } + + return type is INamedTypeSymbol namedType + && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; + } + + private static ITypeSymbol UnwrapNullable(ITypeSymbol type) + { + if (type is INamedTypeSymbol namedType + && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T + && namedType.TypeArguments.Length == 1) + { + return namedType.TypeArguments[0]; + } + + return type; + } + + private static bool IsNullableValueType(ITypeSymbol type) + { + return type is INamedTypeSymbol namedType + && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; + } + + private static bool TryGetCollectionItemType(ITypeSymbol sourceType, out ITypeSymbol itemType) + { + if (sourceType is IArrayTypeSymbol arrayType) + { + itemType = arrayType.ElementType; + return true; + } + + if (sourceType is INamedTypeSymbol namedType + && namedType.IsGenericType + && namedType.TypeArguments.Length == 1) + { + itemType = namedType.TypeArguments[0]; + return true; + } + + itemType = null!; + return false; + } + + private static (string? NamespaceName, string ClassName) SplitClassName(string fullyQualifiedClassName) + { + var lastDotIndex = fullyQualifiedClassName.LastIndexOf('.'); + if (lastDotIndex <= 0 || lastDotIndex == fullyQualifiedClassName.Length - 1) + { + return (null, fullyQualifiedClassName); + } + + return ( + fullyQualifiedClassName.Substring(0, lastDotIndex), + fullyQualifiedClassName.Substring(lastDotIndex + 1)); + } + + private static string GetLineAtIndex(string content, int index) + { + if (index < 0 || index >= content.Length) + { + return string.Empty; + } + + var lineStart = content.LastIndexOf('\n', index); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + + var lineEnd = content.IndexOf('\n', index); + lineEnd = lineEnd < 0 ? content.Length : lineEnd; + + var line = content.Substring(lineStart, lineEnd - lineStart); + if (line.EndsWith("\r", StringComparison.Ordinal)) + { + line = line.Substring(0, line.Length - 1); + } + + return line; + } + + private static string BuildSource( + string? namespaceName, + string className, + ImmutableArray providers) + { + var builder = new StringBuilder(); + builder.AppendLine("// "); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + + if (!string.IsNullOrWhiteSpace(namespaceName)) + { + builder.Append("namespace ").Append(namespaceName).AppendLine(";"); + builder.AppendLine(); + } + + builder.Append("partial class ") + .Append(className) + .Append(" : global::WinUI.TableView.ITableViewConnector") + .AppendLine(); + builder.AppendLine("{"); + builder.AppendLine(" void global::WinUI.TableView.ITableViewConnector.ConnectTableView(global::WinUI.TableView.TableView @param_tableView)"); + builder.AppendLine(" {"); + var isFirstProvider = true; + foreach (var provider in providers) + { + builder.Append(" ") + .Append(isFirstProvider ? "if" : "else if") + .Append(" (global::System.Object.ReferenceEquals(@param_tableView, this.") + .Append(provider.TableViewName) + .AppendLine("))"); + builder.AppendLine(" {"); + builder.Append(" @param_tableView.CellValueProvider = new ") + .Append(provider.ProviderClassName) + .AppendLine("();"); + builder.AppendLine(" }"); + isFirstProvider = false; + } + builder.AppendLine(" }"); + builder.AppendLine(); + + builder.AppendLine(" void global::WinUI.TableView.ITableViewConnector.ConnectTableViews()"); + builder.AppendLine(" {"); + foreach (var provider in providers) + { + builder.Append(" this.") + .Append(provider.TableViewName) + .Append(".CellValueProvider = new ") + .Append(provider.ProviderClassName) + .AppendLine("();"); + } + + builder.AppendLine(" }"); + builder.AppendLine(); + + foreach (var provider in providers) + { + builder.Append(" private sealed class ") + .Append(provider.ProviderClassName) + .Append(" : global::WinUI.TableView.ICellValueProvider") + .AppendLine(); + builder.AppendLine(" {"); + + BuildMethodSource(builder, provider, provider.SortMemberCases, "TryGetSortMemberValue", ColumnPathKind.SortMemberPath); + BuildMethodSource(builder, provider, provider.BindingPathCases, "TryGetBindingValue", ColumnPathKind.BindingPath); + BuildSetterMethodSource(builder, provider); + BuildMethodSource(builder, provider, provider.DisplayMemberCases, "TryGetDisplayMemberValue", ColumnPathKind.DisplayMemberPath); + BuildMethodSource(builder, provider, provider.ClipboardPathCases, "TryGetClipboardContentBindingValue", ColumnPathKind.ClipboardBindingPath); + BuildMethodSource(builder, provider, provider.ContentPathCases, "TryGetContentBindingValue", ColumnPathKind.ContentBindingPath); + + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine("}"); + return builder.ToString(); + } + + private static void BuildMethodSource(StringBuilder builder, GeneratedProviderInfo provider, ImmutableArray memberCases, string methodName, ColumnPathKind pathKind) + { + var itemRootIdentifier = "typedItem"; + + builder.Append(" bool global::WinUI.TableView.ICellValueProvider.") + .Append(methodName) + .Append("(global::System.String? path, global::System.Object? item, out global::System.Object? value)") + .AppendLine(); + builder.AppendLine(" {"); + builder.Append(" var ") + .Append(itemRootIdentifier) + .Append(" = item as ") + .Append(provider.ItemTypeDisplay) + .AppendLine(";"); + builder.AppendLine(); + builder.AppendLine(" switch (path)"); + builder.AppendLine(" {"); + + //itemRootIdentifier = pathKind is ColumnPathKind.DisplayMemberPath ? "typedCellValue" : itemRootIdentifier; + var generatedPaths = new HashSet(StringComparer.Ordinal); + foreach (var memberCase in memberCases) + { + if (!generatedPaths.Add(memberCase.MemberPath)) + { + continue; + } + + + if (memberCase.SourceInfo.HasValue) + { + builder.Append(" // ") + .Append("column line ") + .Append(memberCase.SourceInfo.ColumnStartLine) + .Append(", property line ") + .Append(memberCase.SourceInfo.PathLine) + .AppendLine(); + } + + builder.Append(" case \"") + .Append(memberCase.MemberPath.Replace("\"", "\\\"")) + .Append("\":"); + + if (pathKind is ColumnPathKind.DisplayMemberPath && false) + { + var cellValueCase = provider.BindingPathCases.First(c => c.SourceInfo.ColumnStartLine == memberCase.SourceInfo.ColumnStartLine); + + builder.AppendLine(); + builder.Append(" if (!TryGetCellValue(\"") + .Append(cellValueCase.MemberPath) + .Append("\", item, out var cellValue) || cellValue is not ") + .Append(memberCase.MemberTypeDisplay) + .Append(" ") + .Append(itemRootIdentifier) + .AppendLine(" )"); + builder.AppendLine(" {"); + builder.AppendLine(" value = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + } + + builder.AppendLine(); + builder.Append(" value = ") + .Append(itemRootIdentifier) + .Append("?") + .Append(memberCase.AccessExpression) + .AppendLine(";"); + builder.AppendLine(" return true;"); + } + + builder.AppendLine(" default:"); + builder.AppendLine(" value = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + private static void BuildSetterMethodSource(StringBuilder builder, GeneratedProviderInfo provider) + { + builder.AppendLine(" bool global::WinUI.TableView.ICellValueProvider.TrySetBindingValue(global::System.String? path, global::System.Object? item, global::System.Object? value)"); + builder.AppendLine(" {"); + builder.Append(" if (item is not ") + .Append(provider.ItemTypeDisplay) + .AppendLine(" typedItem)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" switch (path)"); + builder.AppendLine(" {"); + + var generatedPaths = new HashSet(StringComparer.Ordinal); + foreach (var setCase in provider.BindingSetCases) + { + if (!generatedPaths.Add(setCase.MemberPath)) + { + continue; + } + + if (setCase.SourceInfo.HasValue) + { + builder.Append(" // ") + .Append("column line ") + .Append(setCase.SourceInfo.ColumnStartLine) + .Append(", property line ") + .Append(setCase.SourceInfo.PathLine) + .AppendLine(); + } + + builder.Append(" case \"") + .Append(setCase.MemberPath.Replace("\"", "\\\"")) + .AppendLine("\":"); + builder.Append(" return ") + .Append(setCase.LocalMethodName) + .AppendLine("(typedItem, value);"); + } + + builder.AppendLine(" default:"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + foreach (var setCase in provider.BindingSetCases) + { + var isDateTimeTarget = + string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTime", StringComparison.Ordinal) + || string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTime?", StringComparison.Ordinal); + + var isDateTimeOffsetTarget = + string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTimeOffset", StringComparison.Ordinal) + || string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTimeOffset?", StringComparison.Ordinal); + + builder.Append(" static bool ") + .Append(setCase.LocalMethodName) + .Append("(") + .Append(provider.ItemTypeDisplay) + .AppendLine(" typedItem, global::System.Object? value)"); + builder.AppendLine(" {"); + + var currentVariable = "typedItem"; + var segmentIndex = 0; + foreach (var navigationSegment in setCase.NavigationSegments) + { + var nextVariable = "target" + segmentIndex; + builder.Append(" if (") + .Append(currentVariable) + .Append('.') + .Append(navigationSegment.MemberName) + .Append(" is not ") + .Append(navigationSegment.MemberTypeDisplay) + .Append(' ') + .Append(nextVariable) + .AppendLine(")"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + currentVariable = nextVariable; + segmentIndex++; + } + + if (setCase.IsLeafNullableValueType) + { + builder.AppendLine(" if (value is null)"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = null;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + if (isDateTimeOffsetTarget) + { + builder.Append(" var existingValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" if (value is global::System.DateTimeOffset dateInput)"); + builder.AppendLine(" {"); + builder.AppendLine(" var baseValue = existingValue ?? dateInput;"); + builder.AppendLine(" var mergedDateTime = dateInput.Date + baseValue.TimeOfDay;"); + builder.AppendLine(" var mergedValue = new global::System.DateTimeOffset(mergedDateTime, baseValue.Offset);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" if (value is global::System.TimeSpan timeInput)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (timeInput < global::System.TimeSpan.Zero || timeInput >= global::System.TimeSpan.FromDays(1))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var baseValue = existingValue ?? global::System.DateTimeOffset.Now;"); + builder.AppendLine(" var mergedDateTime = baseValue.Date + timeInput;"); + builder.AppendLine(" var mergedValue = new global::System.DateTimeOffset(mergedDateTime, baseValue.Offset);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (isDateTimeTarget) + { + builder.Append(" var existingValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" if (value is global::System.DateTimeOffset dateInput)"); + builder.AppendLine(" {"); + builder.AppendLine(" var baseValue = existingValue ?? dateInput.DateTime;"); + builder.AppendLine(" var dateTicks = new global::System.DateTime(dateInput.Year, dateInput.Month, dateInput.Day).Ticks;"); + builder.AppendLine(" var mergedTicks = dateTicks + baseValue.TimeOfDay.Ticks;"); + builder.AppendLine(" if (mergedTicks < global::System.DateTime.MinValue.Ticks || mergedTicks > global::System.DateTime.MaxValue.Ticks)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var mergedValue = new global::System.DateTime(mergedTicks, baseValue.Kind);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" if (value is global::System.TimeSpan timeInput)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (timeInput < global::System.TimeSpan.Zero || timeInput >= global::System.TimeSpan.FromDays(1))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var baseValue = existingValue ?? global::System.DateTime.Now;"); + builder.AppendLine(" var mergedTicks = baseValue.Date.Ticks + timeInput.Ticks;"); + builder.AppendLine(" if (mergedTicks < global::System.DateTime.MinValue.Ticks || mergedTicks > global::System.DateTime.MaxValue.Ticks)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var mergedValue = new global::System.DateTime(mergedTicks, baseValue.Kind);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasNumericConversion) + { + builder.Append(" if (") + .Append(setCase.NumericConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasDateConversion) + { + builder.Append(" if (") + .Append(setCase.DateConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasTimeConversion) + { + builder.Append(" if (") + .Append(setCase.TimeConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else + { + builder.Append(" if (value is ") + .Append(setCase.LeafValueTypeDisplay) + .AppendLine(" typedValue)"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = typedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine(" return false;"); + } + else + { + if (isDateTimeOffsetTarget) + { + builder.Append(" if (value is global::System.DateTimeOffset dateInput)"); + builder.AppendLine(); + builder.AppendLine(" {"); + builder.Append(" var baseValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" var mergedDateTime = dateInput.Date + baseValue.TimeOfDay;"); + builder.AppendLine(" var mergedValue = new global::System.DateTimeOffset(mergedDateTime, baseValue.Offset);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" if (value is global::System.TimeSpan timeInput)"); + builder.AppendLine(); + builder.AppendLine(" {"); + builder.AppendLine(" if (timeInput < global::System.TimeSpan.Zero || timeInput >= global::System.TimeSpan.FromDays(1))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" var baseValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" var mergedDateTime = baseValue.Date + timeInput;"); + builder.AppendLine(" var mergedValue = new global::System.DateTimeOffset(mergedDateTime, baseValue.Offset);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (isDateTimeTarget) + { + builder.AppendLine(" if (value is global::System.DateTimeOffset dateInput)"); + builder.AppendLine(" {"); + builder.Append(" var baseValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" var dateTicks = new global::System.DateTime(dateInput.Year, dateInput.Month, dateInput.Day).Ticks;"); + builder.AppendLine(" var mergedTicks = dateTicks + baseValue.TimeOfDay.Ticks;"); + builder.AppendLine(" if (mergedTicks < global::System.DateTime.MinValue.Ticks || mergedTicks > global::System.DateTime.MaxValue.Ticks)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var mergedValue = new global::System.DateTime(mergedTicks, baseValue.Kind);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" if (value is global::System.TimeSpan timeInput)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (timeInput < global::System.TimeSpan.Zero || timeInput >= global::System.TimeSpan.FromDays(1))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.Append(" var baseValue = ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(";"); + builder.AppendLine(" var mergedTicks = baseValue.Date.Ticks + timeInput.Ticks;"); + builder.AppendLine(" if (mergedTicks < global::System.DateTime.MinValue.Ticks || mergedTicks > global::System.DateTime.MaxValue.Ticks)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" var mergedValue = new global::System.DateTime(mergedTicks, baseValue.Kind);"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = mergedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasNumericConversion) + { + builder.Append(" if (") + .Append(setCase.NumericConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasDateConversion) + { + builder.Append(" if (") + .Append(setCase.DateConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (setCase.HasTimeConversion) + { + builder.Append(" if (") + .Append(setCase.TimeConversionHelperMethodName) + .AppendLine("(value, out var convertedValue))"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = convertedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else + { + builder.Append(" if (value is ") + .Append(setCase.LeafValueTypeDisplay) + .AppendLine(" typedValue)"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = typedValue;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + if (setCase.CanAssignNull) + { + builder.AppendLine(" if (value is null)"); + builder.AppendLine(" {"); + builder.Append(" ") + .Append(currentVariable) + .Append('.') + .Append(setCase.LeafMemberName) + .AppendLine(" = null;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine(" return false;"); + } + + builder.AppendLine(" }"); + builder.AppendLine(); + } + + var conversionHelpers = new Dictionary(StringComparer.Ordinal); + foreach (var setCase in provider.BindingSetCases) + { + var isDateTimeTarget = + string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTime", StringComparison.Ordinal) + || string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTime?", StringComparison.Ordinal); + + var isDateTimeOffsetTarget = + string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTimeOffset", StringComparison.Ordinal) + || string.Equals(setCase.LeafValueTypeDisplay, "global::System.DateTimeOffset?", StringComparison.Ordinal); + + if (setCase.HasNumericConversion + && !conversionHelpers.ContainsKey(setCase.NumericConversionHelperMethodName)) + { + conversionHelpers.Add(setCase.NumericConversionHelperMethodName, setCase); + } + + if (!isDateTimeTarget + && !isDateTimeOffsetTarget + && setCase.HasDateConversion + && !conversionHelpers.ContainsKey(setCase.DateConversionHelperMethodName)) + { + conversionHelpers.Add(setCase.DateConversionHelperMethodName, setCase); + } + + if (!isDateTimeTarget + && !isDateTimeOffsetTarget + && setCase.HasTimeConversion + && !conversionHelpers.ContainsKey(setCase.TimeConversionHelperMethodName)) + { + conversionHelpers.Add(setCase.TimeConversionHelperMethodName, setCase); + } + } + + foreach (var helper in conversionHelpers.Values) + { + if (helper.HasNumericConversion) + { + builder.Append(" static bool ") + .Append(helper.NumericConversionHelperMethodName) + .Append("(global::System.Object? input, out ") + .Append(helper.LeafValueTypeDisplay) + .AppendLine(" converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.Double number)"); + builder.AppendLine(" {"); + if (helper.NumericConvertMethodName == "ToDouble") + { + builder.AppendLine(" converted = number;"); + builder.AppendLine(" return true;"); + } + else + { + builder.AppendLine(" try"); + builder.AppendLine(" {"); + builder.Append(" converted = global::System.Convert.") + .Append(helper.NumericConvertMethodName) + .AppendLine("(number);"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(" catch (global::System.Exception)"); + builder.AppendLine(" {"); + builder.AppendLine(" }"); + } + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.HasDateConversion) + { + if (helper.DateConversionHelperMethodName == "TryGetDateOnlyFromDateTimeOffset") + { + builder.AppendLine(" static bool TryGetDateOnlyFromDateTimeOffset(global::System.Object? input, out global::System.DateOnly converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.DateTimeOffset dateTimeOffset)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = global::System.DateOnly.FromDateTime(dateTimeOffset.DateTime);"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.DateConversionHelperMethodName == "TryGetDateTimeFromDateTimeOffset") + { + builder.AppendLine(" static bool TryGetDateTimeFromDateTimeOffset(global::System.Object? input, out global::System.DateTime converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.DateTimeOffset dateTimeOffset)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = dateTimeOffset.DateTime;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.DateConversionHelperMethodName == "TryGetDateTimeOffsetFromDateTimeOffset") + { + builder.AppendLine(" static bool TryGetDateTimeOffsetFromDateTimeOffset(global::System.Object? input, out global::System.DateTimeOffset converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.DateTimeOffset dateTimeOffset)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = dateTimeOffset;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + } + else if (helper.HasTimeConversion) + { + if (helper.TimeConversionHelperMethodName == "TryGetTimeOnlyFromTimeSpan") + { + builder.AppendLine(" static bool TryGetTimeOnlyFromTimeSpan(global::System.Object? input, out global::System.TimeOnly converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.TimeSpan timeSpan)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = global::System.TimeOnly.FromTimeSpan(timeSpan);"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.TimeConversionHelperMethodName == "TryGetTimeSpanFromTimeSpan") + { + builder.AppendLine(" static bool TryGetTimeSpanFromTimeSpan(global::System.Object? input, out global::System.TimeSpan converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.TimeSpan timeSpan)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = timeSpan;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.TimeConversionHelperMethodName == "TryGetDateTimeFromTimeSpan") + { + builder.AppendLine(" static bool TryGetDateTimeFromTimeSpan(global::System.Object? input, out global::System.DateTime converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.TimeSpan timeSpan)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = global::System.DateTime.MinValue.Add(timeSpan);"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + else if (helper.TimeConversionHelperMethodName == "TryGetDateTimeOffsetFromTimeSpan") + { + builder.AppendLine(" static bool TryGetDateTimeOffsetFromTimeSpan(global::System.Object? input, out global::System.DateTimeOffset converted)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input is global::System.TimeSpan timeSpan)"); + builder.AppendLine(" {"); + builder.AppendLine(" converted = new global::System.DateTimeOffset(global::System.DateTime.MinValue.Add(timeSpan), global::System.TimeSpan.Zero);"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" converted = default;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + } + } + + builder.AppendLine(" }"); + builder.AppendLine(); + } + + private static string EnsureGlobalQualified(string typeDisplay) + { + var trimmed = NormalizeSpecialTypeAlias(typeDisplay.Trim()); + return trimmed.StartsWith("global::", StringComparison.Ordinal) + ? trimmed + : "global::" + trimmed; + } + + private static string GetGlobalTypeDisplay(ITypeSymbol type) + { + return EnsureGlobalQualified(type.ToDisplayString(FullyQualifiedNonAliasedTypeFormat)); + } + + private static string NormalizeSpecialTypeAlias(string typeDisplay) + { + return typeDisplay switch + { + "bool" => "System.Boolean", + "byte" => "System.Byte", + "sbyte" => "System.SByte", + "short" => "System.Int16", + "ushort" => "System.UInt16", + "int" => "System.Int32", + "uint" => "System.UInt32", + "long" => "System.Int64", + "ulong" => "System.UInt64", + "char" => "System.Char", + "float" => "System.Single", + "double" => "System.Double", + "decimal" => "System.Decimal", + "string" => "System.String", + "object" => "System.Object", + "nint" => "System.IntPtr", + "nuint" => "System.UIntPtr", + "bool?" => "System.Boolean?", + "byte?" => "System.Byte?", + "sbyte?" => "System.SByte?", + "short?" => "System.Int16?", + "ushort?" => "System.UInt16?", + "int?" => "System.Int32?", + "uint?" => "System.UInt32?", + "long?" => "System.Int64?", + "ulong?" => "System.UInt64?", + "char?" => "System.Char?", + "float?" => "System.Single?", + "double?" => "System.Double?", + "decimal?" => "System.Decimal?", + "nint?" => "System.IntPtr?", + "nuint?" => "System.UIntPtr?", + _ => typeDisplay + }; + } + + private static string SanitizeIdentifier(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "Unnamed"; + } + + var buffer = new StringBuilder(value.Length); + for (var i = 0; i < value.Length; i++) + { + var ch = value[i]; + var valid = i == 0 ? char.IsLetter(ch) || ch == '_' : char.IsLetterOrDigit(ch) || ch == '_'; + buffer.Append(valid ? ch : '_'); + } + + if (buffer.Length == 0 || !(char.IsLetter(buffer[0]) || buffer[0] == '_')) + { + buffer.Insert(0, '_'); + } + + return buffer.ToString(); + } + + private static string GetUniqueIdentifier(string baseName, HashSet used) + { + var candidate = baseName; + var suffix = 1; + while (!used.Add(candidate)) + { + candidate = baseName + "_" + suffix; + suffix++; + } + + return candidate; + } + + +} + diff --git a/generators/WinUI.TableView.SourceGenerators.csproj b/generators/WinUI.TableView.SourceGenerators.csproj new file mode 100644 index 00000000..4a4a16f0 --- /dev/null +++ b/generators/WinUI.TableView.SourceGenerators.csproj @@ -0,0 +1,19 @@ + + + netstandard2.0 + 12 + enable + enable + true + + + + + + + + + + + + diff --git a/samples/AotTestApp/AotTestApp.csproj b/samples/AotTestApp/AotTestApp.csproj new file mode 100644 index 00000000..0dfe9929 --- /dev/null +++ b/samples/AotTestApp/AotTestApp.csproj @@ -0,0 +1,74 @@ + + + WinExe + net10.0-windows10.0.19041.0 + 10.0.17763.0 + AotTestApp + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + true + false + true + enable + true + true + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + False + True + False + True + + \ No newline at end of file diff --git a/samples/AotTestApp/App.xaml b/samples/AotTestApp/App.xaml new file mode 100644 index 00000000..46f6f503 --- /dev/null +++ b/samples/AotTestApp/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/samples/AotTestApp/App.xaml.cs b/samples/AotTestApp/App.xaml.cs new file mode 100644 index 00000000..a068abaa --- /dev/null +++ b/samples/AotTestApp/App.xaml.cs @@ -0,0 +1,48 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.UI.Xaml.Shapes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace AotTestApp; +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : Application +{ + private Window? _window; + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + _window = new MainWindow(); + _window.Activate(); + } +} diff --git a/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png b/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..7440f0d4 Binary files /dev/null and b/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/SplashScreen.scale-200.png b/samples/AotTestApp/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000..32f486a8 Binary files /dev/null and b/samples/AotTestApp/Assets/SplashScreen.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png b/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..53ee3777 Binary files /dev/null and b/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png b/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..f713bba6 Binary files /dev/null and b/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..dc9f5bea Binary files /dev/null and b/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/samples/AotTestApp/Assets/StoreLogo.png b/samples/AotTestApp/Assets/StoreLogo.png new file mode 100644 index 00000000..a4586f26 Binary files /dev/null and b/samples/AotTestApp/Assets/StoreLogo.png differ diff --git a/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png b/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..8b4a5d0d Binary files /dev/null and b/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png differ diff --git a/samples/AotTestApp/BlankPage1.xaml b/samples/AotTestApp/BlankPage1.xaml new file mode 100644 index 00000000..089b6dd5 --- /dev/null +++ b/samples/AotTestApp/BlankPage1.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AotTestApp/BlankPage1.xaml.cs b/samples/AotTestApp/BlankPage1.xaml.cs new file mode 100644 index 00000000..d5881fe9 --- /dev/null +++ b/samples/AotTestApp/BlankPage1.xaml.cs @@ -0,0 +1,36 @@ +using ABI.System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using WinUI.TableView; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace AotTestApp; +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class BlankPage1 : Page +{ + public BlankPage1() + { + InitializeComponent(); + + MainViewModel.InitializeItems(); + ViewModel = new MainViewModel(); + } + + public MainViewModel ViewModel { get; } +} diff --git a/samples/AotTestApp/DataFaker.cs b/samples/AotTestApp/DataFaker.cs new file mode 100644 index 00000000..da9fb7c0 --- /dev/null +++ b/samples/AotTestApp/DataFaker.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; + +namespace AotTestApp; + +/// +/// A native AOT-compatible data faker for generating sample data. +/// +public static class DataFaker +{ + private static readonly Random _random = new(); + + // First names + private static readonly string[] FirstNames = + [ + "James", "Mary", "Robert", "Patricia", "Michael", "Jennifer", "William", "Linda", "David", "Barbara", + "Richard", "Elizabeth", "Joseph", "Susan", "Thomas", "Jessica", "Charles", "Sarah", "Christopher", "Karen", + "Daniel", "Nancy", "Matthew", "Lisa", "Anthony", "Betty", "Mark", "Margaret", "Donald", "Sandra", + "Steven", "Ashley", "Paul", "Kimberly", "Andrew", "Donna", "Joshua", "Carol", "Kenneth", "Michelle", + "Kevin", "Emily", "Brian", "Melissa", "George", "Deborah", "Edward", "Stephanie", "Ronald", "Rebecca", + "Timothy", "Sharon", "Jason", "Brenda", "Jeffrey", "Amber", "Ryan", "Anna", "Jacob", "Pamela", + "Gary", "Nicole", "Nicholas", "Emma", "Eric", "Helen", "Jonathan", "Samantha", "Stephen", "Katherine" + ]; + + // Last names + private static readonly string[] LastNames = + [ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", + "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", + "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", + "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Peterson", "Phillips", "Campbell", + "Parker", "Evans", "Edwards", "Collins", "Reyes", "Stewart", "Morris", "Morales", "Murphy", "Cook", + "Rogers", "Morgan", "Peterson", "Cooper", "Reed", "Bell", "Gomez", "Murray", "Freeman", "Wells", + "Webb", "Simpson", "Stevens", "Tucker", "Porter", "Hunter", "Hicks", "Crawford", "Henry", "Boyd" + ]; + + // Job titles + internal static readonly string[] JobTitles = + [ + "Software Developer", + "Manager", + "Sales Representative", + "Accountant", + "Analyst", + "Engineer", + "Designer", + "Teacher", + "Consultant", + "Executive", + "Administrator", + "Coordinator", + "Director", + "Supervisor", + "Specialist", + "Technician", + "Operator", + "Clerk", + "Assistant", + "Officer", + "Agent", + "Associate", + "Architect", + "Planner", + "Scientist", + "Researcher", + "Programmer", + "Nurse", + "Doctor", + "Lawyer", + "Marketing Manager", + "Product Manager", + "Business Analyst", + "Data Scientist", + "DevOps Engineer" + ]; + + // Department names + internal static readonly string[] Departments = + [ + "Sales", "Marketing", "Engineering", "Finance", "Human Resources", + "Operations", "IT", "Legal", "Research", "Development", + "Quality Assurance", "Customer Service", "Production", "Logistics", "Planning", + "Maintenance", "Administration", "Strategic Planning", "Corporate Communications", "Treasury" + ]; + + // Street suffixes + private static readonly string[] StreetSuffixes = + [ + "Street", "Avenue", "Road", "Boulevard", "Drive", "Lane", "Court", "Circle", + "Way", "Parkway", "Plaza", "Square", "Trail", "Ridge", "Hill", "Oak" + ]; + + // Cities + private static readonly string[] Cities = + [ + "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", + "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose", + "Austin", "Jacksonville", "Seattle", "Denver", "Boston", + "Portland", "Miami", "Atlanta", "Las Vegas", "Detroit" + ]; + + // States + private static readonly string[] States = + [ + "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY" + ]; + + private static readonly string[] Regions = [ + "East", "West", "North", "South" + ]; + + // Genders + internal static readonly string[] Genders = ["Male", "Female", "Non-binary", "Genderfluid", "Agender", "Bigender", "Genderqueer", "Two-Spirit", "Prefer not to say"]; + + // Avatar images (using placeholder service URLs) + private static readonly string[] AvatarSeeds = + [ + "avatar1", "avatar2", "avatar3", "avatar4", "avatar5", + "avatar6", "avatar7", "avatar8", "avatar9", "avatar10" + ]; + + public static string FirstName() + { + return FirstNames[_random.Next(FirstNames.Length)]; + } + + public static string LastName() + { + return LastNames[_random.Next(LastNames.Length)]; + } + + public static string FullName() + { + return $"{FirstName()} {LastName()}"; + } + + public static string Email(string? firstName = null, string? lastName = null) + { + firstName ??= FirstName(); + lastName ??= LastName(); + var domains = new[] { "example.com", "test.com", "sample.com", "data.com" }; + var domain = domains[_random.Next(domains.Length)]; + return $"{firstName.ToLower()}.{lastName.ToLower()}@{domain}"; + } + + public static string Gender() + { + return Genders[_random.Next(Genders.Length)]; + } + + public static DateOnly PastDate(int yearsBack = 50, DateOnly? maxDate = null) + { + maxDate ??= DateOnly.FromDateTime(DateTime.Now); + var startDate = maxDate.Value.AddYears(-yearsBack); + var daysRange = maxDate.Value.DayNumber - startDate.DayNumber; + var randomDays = _random.Next(daysRange); + return startDate.AddDays(randomDays); + } + + public static TimeOnly TimeOfDay() + { + var hours = _random.Next(0, 24); + var minutes = _random.Next(0, 60); + var seconds = _random.Next(0, 60); + return new TimeOnly(hours, minutes, seconds); + } + + public static bool Boolean(float truePercentage = 0.5f) + { + return _random.NextSingle() < truePercentage; + } + + public static int Integer(int min = 0, int max = int.MaxValue) + { + return _random.Next(min, max); + } + + public static decimal Decimal(decimal min = 0, decimal max = 1000) + { + return ((decimal)_random.NextDouble() * (max - min)) + min; + } + + public static string Department() + { + return Departments[_random.Next(Departments.Length)]; + } + + public static string JobTitle() + { + return JobTitles[_random.Next(JobTitles.Length)]; + } + + public static string Address() + { + var streetNumber = _random.Next(1, 9999); + var streetName = FirstName(); + var suffix = StreetSuffixes[_random.Next(StreetSuffixes.Length)]; + var city = Cities[_random.Next(Cities.Length)]; + var state = States[_random.Next(States.Length)]; + var zip = _random.Next(10000, 99999); + return $"{streetNumber} {streetName} {suffix}, {city}, {state} {zip}"; + } + + public static string Avatar() + { + var seed = AvatarSeeds[_random.Next(AvatarSeeds.Length)]; + var id = _random.Next(1, 100); + return $"https://api.dicebear.com/7.x/avataaars/svg?seed={seed}{id}"; + } + + public static string ZipCode() + { + return _random.Next(10000, 99999).ToString(); + } + + public static string State() + { + return States[_random.Next(States.Length)]; + } + + public static string Region() + { + return Regions[_random.Next(Regions.Length)]; + } + + public static string City() + { + return Cities[_random.Next(Cities.Length)]; + } +} diff --git a/samples/AotTestApp/ExampleModel.cs b/samples/AotTestApp/ExampleModel.cs new file mode 100644 index 00000000..4a453f20 --- /dev/null +++ b/samples/AotTestApp/ExampleModel.cs @@ -0,0 +1,94 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace AotTestApp; + +public partial class Designation : ObservableObject +{ + public Designation(int id, string? title) + { + Id = id; + Title = title; + } + + [ObservableProperty] + public partial int Id { get; set; } + + [ObservableProperty] + public partial string? Title { get; set; } +} + +public partial class User : ObservableObject +{ + [ObservableProperty] + [Display(ShortName = "First Name")] + public partial string? FirstName { get; set; } + + [ObservableProperty] + [Display(ShortName = "Last Name")] + public partial string? LastName { get; set; } + + [ObservableProperty] + public partial string? Email { get; set; } + + [ObservableProperty] + public partial string? Gender { get; set; } + + [ObservableProperty] + public partial DateOnly Dob { get; set; } +} + +public partial class DateTimeModel : ObservableObject +{ + + [ObservableProperty] + public partial TimeSpan TimeSpan1 { get; set; } + + [ObservableProperty] + public partial TimeOnly TimeOnly1 { get; set; } + + [ObservableProperty] + public partial DateOnly DateOnly1 { get; set; } + + [ObservableProperty] + public partial DateTime DateTime1 { get; set; } + + [ObservableProperty] + public partial DateTimeOffset DateTimeOffset1 { get; set; } +} + +public partial class ExampleModel : ObservableObject +{ + [ObservableProperty] + public partial int Id { get; set; } + + [ObservableProperty] + [Display(ShortName = "Active At")] + public partial TimeOnly ActiveAt { get; set; } + + [ObservableProperty] + [Display(ShortName = "Is Active")] + public partial bool IsActive { get; set; } + + [ObservableProperty] + public partial string? Department { get; set; } + + [ObservableProperty] + public partial string? Designation { get; set; } + + [ObservableProperty] + public partial string? Address { get; set; } + + [ObservableProperty] + [Display(AutoGenerateField = false)] + public partial string? Avatar { get; set; } + + public Uri AvatarUrl => new(Avatar ?? string.Empty); + + [ObservableProperty] + [Display(AutoGenerateField = false)] + public partial User? User { get; set; } + + [ObservableProperty] + public partial DateTimeModel? DateTimeModel { get; set; } +} diff --git a/samples/AotTestApp/MainViewModel.cs b/samples/AotTestApp/MainViewModel.cs new file mode 100644 index 00000000..d69ae030 --- /dev/null +++ b/samples/AotTestApp/MainViewModel.cs @@ -0,0 +1,76 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace AotTestApp; + +public partial class MainViewModel : ObservableObject +{ + public MainViewModel() + { + foreach (var item in ItemsList) + { + Items.Add(item); + } + } + + public static void InitializeItems() + { + var startId = 1; + var startDate = new DateOnly(1970, 1, 1); + + ItemsList.Clear(); + + for (var i = 0; i < 1_000; i++) + { + var firstName = DataFaker.FirstName(); + var lastName = DataFaker.LastName(); + var email = DataFaker.Email(firstName, lastName); + var gender = DataFaker.Gender(); + var dob = DataFaker.PastDate(50, startDate); + var item = new ExampleModel + { + Id = startId++, + IsActive = DataFaker.Boolean(), + ActiveAt = DataFaker.TimeOfDay(), + Department = DataFaker.Department(), + Designation = DataFaker.JobTitle(), + Address = DataFaker.Address(), + Avatar = DataFaker.Avatar(), + User = new User + { + FirstName = firstName, + LastName = lastName, + Email = email, + Gender = gender, + Dob = dob + }, + DateTimeModel = new DateTimeModel + { + TimeSpan1 = DataFaker.TimeOfDay().ToTimeSpan(), + TimeOnly1 = DataFaker.TimeOfDay(), + DateTime1 = new DateTime(DataFaker.PastDate(50, dob), DataFaker.TimeOfDay()), + DateTimeOffset1 = new DateTimeOffset(DataFaker.PastDate(50, dob), DataFaker.TimeOfDay(), TimeSpan.Zero), + DateOnly1 = DataFaker.PastDate(50, dob) + } + }; + ItemsList.Add(item); + } + } + + public static List ItemsList { get; } = []; + + [ObservableProperty] + public partial ObservableCollection Items { get; set; } = []; + + public List Genders => [.. DataFaker.Genders]; + + public List Departments => [.. DataFaker.Departments]; + + public List Designations => [.. DataFaker.JobTitles]; + [ObservableProperty] + public partial ExampleModel? SelectedItem { get; set; } +} + diff --git a/samples/AotTestApp/MainWindow.xaml b/samples/AotTestApp/MainWindow.xaml new file mode 100644 index 00000000..396c5d45 --- /dev/null +++ b/samples/AotTestApp/MainWindow.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/samples/AotTestApp/MainWindow.xaml.cs b/samples/AotTestApp/MainWindow.xaml.cs new file mode 100644 index 00000000..fd4d239a --- /dev/null +++ b/samples/AotTestApp/MainWindow.xaml.cs @@ -0,0 +1,12 @@ +using Microsoft.UI.Xaml; +using WinUI.TableView; + +namespace AotTestApp; + +public sealed partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/AotTestApp/Package.appxmanifest b/samples/AotTestApp/Package.appxmanifest new file mode 100644 index 00000000..b6a63309 --- /dev/null +++ b/samples/AotTestApp/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + AotTestApp + WaheedAhmed + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AotTestApp/Properties/launchSettings.json b/samples/AotTestApp/Properties/launchSettings.json new file mode 100644 index 00000000..d571cee3 --- /dev/null +++ b/samples/AotTestApp/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "AotTestApp (Package)": { + "commandName": "MsixPackage" + }, + "AotTestApp (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/samples/AotTestApp/app.manifest b/samples/AotTestApp/app.manifest new file mode 100644 index 00000000..c5cdad37 --- /dev/null +++ b/samples/AotTestApp/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/ColumnFilterHandler.cs b/src/ColumnFilterHandler.cs index 9c6e94cd..8a1b67f3 100644 --- a/src/ColumnFilterHandler.cs +++ b/src/ColumnFilterHandler.cs @@ -69,13 +69,13 @@ private IEnumerable GetFilterItemsWithCount(TableViewColumn else filterValues.Add(value, 1); } - IEnumerable nullFilterItem = nullCount > 0 ? [new TableViewFilterItem(isNullItemSelected, null, nullCount)] : []; + IEnumerable nullFilterItem = nullCount > 0 ? [new TableViewFilterItem(isNullItemSelected, null, nullCount, true)] : []; return [.. nullFilterItem,.. filterValues.Select(x => { var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) || (column.IsFiltered && SelectedValues[column].Contains(x.Key)); - return new TableViewFilterItem(isSelected, x.Key, x.Value); + return new TableViewFilterItem(isSelected, x.Key, x.Value, true); }) .OrderByDescending(x=>x.Count)]; } diff --git a/src/Columns/TableViewBoundColumn.cs b/src/Columns/TableViewBoundColumn.cs index 39673298..21cadf50 100644 --- a/src/Columns/TableViewBoundColumn.cs +++ b/src/Columns/TableViewBoundColumn.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Reflection; using WinUI.TableView.Extensions; +using WinUI.TableView.Helpers; namespace WinUI.TableView; @@ -18,6 +19,12 @@ public abstract class TableViewBoundColumn : TableViewColumn /// public override object? GetCellContent(object? dataItem) { + if (TableView?.CellValueProvider is { } provider && + provider.TryGetBindingValue(PropertyPath, dataItem, out var value)) + { + return BindingHelper.ConvertValue(Binding, value); + } + if (dataItem is null) return null; @@ -39,6 +46,21 @@ public abstract class TableViewBoundColumn : TableViewColumn return dataItem; } + /// + /// Sets the value of the cell for the specified data item based on the column's binding. + /// + /// The data item for which the value is being set. + /// The value to set. + public virtual void TrySetBindingValue(object? dataItem, object? value) + { + var convertedValue = BindingHelper.ConvertBackValue(Binding, value); + + if (TableView?.CellValueProvider is { } provider) + { + provider.TrySetBindingValue(PropertyPath, dataItem, convertedValue); + } + } + /// /// Gets the property path for the binding. /// @@ -62,6 +84,8 @@ public virtual Binding Binding { value.UpdateSourceTrigger = UpdateSourceTrigger.Explicit; } + + IsReadOnly = value.Mode == BindingMode.OneWay; } _binding = value!; diff --git a/src/Columns/TableViewCheckBoxColumn.cs b/src/Columns/TableViewCheckBoxColumn.cs index 56592ad9..08f10dee 100644 --- a/src/Columns/TableViewCheckBoxColumn.cs +++ b/src/Columns/TableViewCheckBoxColumn.cs @@ -37,9 +37,9 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat Margin = new Thickness(12, 0, 12, 0), HorizontalAlignment = HorizontalAlignment.Center, UseSystemFocusVisuals = false, + IsChecked = GetCellContent(dataItem) as bool? }; - checkBox.SetBinding(ToggleButton.IsCheckedProperty, Binding); UpdateCheckBoxState(checkBox); return checkBox; @@ -51,6 +51,15 @@ public override FrameworkElement GenerateEditingElement(TableViewCell cell, obje throw new NotImplementedException(); } + /// + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not CheckBox checkBox) + base.RefreshElement(cell, dataItem); + else + checkBox.IsChecked = GetCellContent(dataItem) as bool?; + } + /// public override void UpdateElementState(TableViewCell cell, object? dataItem) { @@ -61,14 +70,14 @@ public override void UpdateElementState(TableViewCell cell, object? dataItem) } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is CheckBox checkBox) { return checkBox.IsChecked; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// @@ -78,8 +87,7 @@ protected internal override void EndCellEditing(TableViewCell cell, object? data { if (editAction == TableViewEditAction.Commit) { - var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty); - bindingExpression?.UpdateSource(); + TrySetBindingValue(dataItem, checkBox.IsChecked); } } } diff --git a/src/Columns/TableViewColumn.cs b/src/Columns/TableViewColumn.cs index 48ae832a..8951ed3b 100644 --- a/src/Columns/TableViewColumn.cs +++ b/src/Columns/TableViewColumn.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using WinUI.TableView.Extensions; +using WinUI.TableView.Helpers; using SD = WinUI.TableView.SortDirection; namespace WinUI.TableView; @@ -56,9 +57,10 @@ public virtual void RefreshElement(TableViewCell cell, object? dataItem) { } /// Called to prepare the cell for editing. /// /// The cell to prepare for editing. + /// The data item associated with the cell. /// The routed event. /// Should return the unedited cell value. - protected internal virtual object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + protected internal virtual object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { return default; } @@ -117,6 +119,12 @@ internal void SetOwningTableView(TableView tableView) /// The clipboard content of the cell. public virtual object? GetClipboardContent(object? dataItem) { + if (TableView?.CellValueProvider is { } provider && + provider.TryGetClipboardContentBindingValue(ClipboardContentBindingPropertyPath, dataItem, out var value)) + { + return BindingHelper.ConvertValue(ClipboardContentBinding, value); + } + if (dataItem is null) return null; diff --git a/src/Columns/TableViewComboBoxColumn.cs b/src/Columns/TableViewComboBoxColumn.cs index dcd4472e..6c51b81f 100644 --- a/src/Columns/TableViewComboBoxColumn.cs +++ b/src/Columns/TableViewComboBoxColumn.cs @@ -29,19 +29,22 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat var textBlock = new TextBlock { Margin = new Thickness(12, 0, 12, 0), + Text = GetDisplayValue(dataItem) }; - if (!string.IsNullOrEmpty(DisplayMemberPath)) - { - textBlock.SetBinding(FrameworkElement.DataContextProperty, Binding); - textBlock.SetBinding(TextBlock.TextProperty, new Binding { Path = new PropertyPath(DisplayMemberPath) }); - } - else + return textBlock; + } + + /// + protected virtual string? GetDisplayValue(object? dataItem) + { + if (TableView?.CellValueProvider is { } provider && + provider.TryGetDisplayMemberValue(DisplayMemberPath, dataItem, out var value)) { - textBlock.SetBinding(TextBlock.TextProperty, Binding); + return value?.ToString(); } - return textBlock; + return GetCellContent(dataItem)?.ToString(); } /// @@ -52,35 +55,49 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat /// A ComboBox element. public override FrameworkElement GenerateEditingElement(TableViewCell cell, object? dataItem) { - var comboBox = new ComboBox { HorizontalAlignment = HorizontalAlignment.Stretch }; - comboBox.SetBinding(ItemsControl.ItemsSourceProperty, new Binding { Source = this, Path = new PropertyPath(nameof(ItemsSource)) }); - comboBox.SetBinding(Selector.SelectedValuePathProperty, new Binding { Source = this, Path = new PropertyPath(nameof(SelectedValuePath)) }); - comboBox.SetBinding(ItemsControl.DisplayMemberPathProperty, new Binding { Source = this, Path = new PropertyPath(nameof(DisplayMemberPath)) }); - comboBox.SetBinding(Selector.SelectedItemProperty, Binding); - comboBox.SetBinding(ComboBox.IsEditableProperty, new Binding { Source = this, Path = new PropertyPath(nameof(IsEditable)) }); - - if (TextBinding is not null) + var comboBox = new ComboBox { - comboBox.SetBinding(ComboBox.TextProperty, TextBinding); - } + HorizontalAlignment = HorizontalAlignment.Stretch, + SelectedValuePath = SelectedValuePath, + DisplayMemberPath = DisplayMemberPath, + Text = GetDisplayValue(dataItem), + SelectedItem = GetCellContent(dataItem), + ItemsSource = ItemsSource, + IsEditable = IsEditable, + }; - if (SelectedValueBinding is not null) - { - comboBox.SetBinding(Selector.SelectedValueProperty, SelectedValueBinding); - } + //if (TextBinding is not null) + //{ + // comboBox.SetBinding(ComboBox.TextProperty, TextBinding); + //} + + //if (SelectedValueBinding is not null) + //{ + // comboBox.SetBinding(Selector.SelectedValueProperty, SelectedValueBinding); + //} return comboBox; } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not TextBlock textBlock) + base.RefreshElement(cell, dataItem); + else + textBlock.Text = GetDisplayValue(dataItem); + } + + /// + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is ComboBox comboBox) { + return comboBox.SelectedItem; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// @@ -90,8 +107,7 @@ protected internal override void EndCellEditing(TableViewCell cell, object? data { if (editAction == TableViewEditAction.Commit) { - var bindingExpression = comboBox.GetBindingExpression(Selector.SelectedItemProperty); - bindingExpression?.UpdateSource(); + TrySetBindingValue(dataItem, comboBox.SelectedItem); } } } diff --git a/src/Columns/TableViewDateColumn.cs b/src/Columns/TableViewDateColumn.cs index 1c3db4e6..af6d85c5 100644 --- a/src/Columns/TableViewDateColumn.cs +++ b/src/Columns/TableViewDateColumn.cs @@ -13,12 +13,17 @@ namespace WinUI.TableView; /// Represents a column in a TableView that displays a date. /// [StyleTypedProperty(Property = nameof(ElementStyle), StyleTargetType = typeof(TextBlock))] -[StyleTypedProperty(Property = nameof(EditingElementStyle), StyleTargetType = typeof(TableViewDatePicker))] +[StyleTypedProperty(Property = nameof(EditingElementStyle), StyleTargetType = typeof(CalendarDatePicker))] #if WINDOWS [WinRT.GeneratedBindableCustomProperty] #endif public partial class TableViewDateColumn : TableViewBoundColumn { + /// + /// Default date format used for displaying the date in the cell when DateFormat is not set. + /// + public const string DefaultDateFormat = "shortdate"; + /// /// Generates a TextBlock element for the cell. /// @@ -31,13 +36,8 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat { Margin = new Thickness(12, 0, 12, 0), }; - - textBlock.SetBinding(DateTimeFormatHelper.ValueProperty, Binding); - textBlock.SetBinding(DateTimeFormatHelper.FormatProperty, new Binding - { - Path = new PropertyPath(nameof(DateFormat)), - Source = this - }); + DateTimeFormatHelper.SetValue(textBlock, GetCellContent(dataItem)); + DateTimeFormatHelper.SetFormat(textBlock, DateFormat ?? DefaultDateFormat); return textBlock; } @@ -50,7 +50,15 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat /// A TableViewDatePicker element. public override FrameworkElement GenerateEditingElement(TableViewCell cell, object? dataItem) { - var timePicker = new TableViewDatePicker + var date = GetCellContent(dataItem) switch + { + DateOnly dateOnly => dateOnly.ToDateTimeOffset(), + DateTime dateTime => dateTime.ToDateTimeOffset(), + DateTimeOffset dateTimeOffset => dateTimeOffset, + _ => default + }; + + var datePicker = new CalendarDatePicker { MinDate = MinDate, MaxDate = MaxDate, @@ -61,66 +69,44 @@ public override FrameworkElement GenerateEditingElement(TableViewCell cell, obje PlaceholderText = PlaceHolderText ?? TableViewLocalizedStrings.DatePickerPlaceholder, DayOfWeekFormat = DayOfWeekFormat, FirstDayOfWeek = FirstDayOfWeek, - SourceType = GetSourcePropertyType(dataItem), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, + Date = date, }; - timePicker.SetBinding(TableViewDatePicker.SelectedDateProperty, Binding); - return timePicker; + return datePicker; } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + public override void RefreshElement(TableViewCell cell, object? dataItem) { - if (cell.Content is TableViewDatePicker datePicker) - { - return datePicker.SelectedDate; - } - - return base.PrepareCellForEdit(cell, routedEvent); + if (cell.Content is not TextBlock textBlock) + base.RefreshElement(cell, dataItem); + else + DateTimeFormatHelper.SetValue(textBlock, GetCellContent(dataItem)); } /// - protected internal override void EndCellEditing(TableViewCell cell, object? dataItem, TableViewEditAction editAction, object? uneditedValue) + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { - if (cell.Content is TableViewDatePicker datePicker) + if (cell.Content is CalendarDatePicker datePicker) { - if (editAction == TableViewEditAction.Commit) - { - var bindingExpression = datePicker.GetBindingExpression(TableViewDatePicker.SelectedDateProperty); - bindingExpression?.UpdateSource(); - } + return datePicker.Date; } + + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } - /// - /// Gets the type of the source property. - /// - /// The data item associated with the cell. - /// The type of the source property. - private Type? GetSourcePropertyType(object? dataItem) + /// + protected internal override void EndCellEditing(TableViewCell cell, object? dataItem, TableViewEditAction editAction, object? uneditedValue) { - if (Binding is not null && dataItem is not null) + if (cell.Content is CalendarDatePicker datePicker) { - var type = dataItem.GetType(); - - if (!string.IsNullOrEmpty(PropertyPath)) - { - var propertyInfo = type.GetProperty(PropertyPath); - if (propertyInfo is not null) - { - type = propertyInfo.PropertyType; - } - } - - if (type.IsDateOnly() || type.IsDateTime() || type.IsDateTimeOffset()) + if (editAction == TableViewEditAction.Commit) { - return type; + TrySetBindingValue(dataItem, datePicker.Date); } } - - return typeof(DateTimeOffset); } /// @@ -247,5 +233,5 @@ public DayOfWeek FirstDayOfWeek /// /// Identifies the DateFormat dependency property. /// - public static readonly DependencyProperty DateFormatProperty = DependencyProperty.Register(nameof(DateFormat), typeof(string), typeof(TableViewDateColumn), new PropertyMetadata("shortdate")); + public static readonly DependencyProperty DateFormatProperty = DependencyProperty.Register(nameof(DateFormat), typeof(string), typeof(TableViewDateColumn), new PropertyMetadata(DefaultDateFormat)); } diff --git a/src/Columns/TableViewHyperlinkColumn.cs b/src/Columns/TableViewHyperlinkColumn.cs index 7c408bc2..52435110 100644 --- a/src/Columns/TableViewHyperlinkColumn.cs +++ b/src/Columns/TableViewHyperlinkColumn.cs @@ -1,6 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; +using System; namespace WinUI.TableView; @@ -26,17 +27,62 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat { Margin = new Thickness(2, 0, 2, 0), HorizontalAlignment = HorizontalAlignment.Stretch, - HorizontalContentAlignment = HorizontalAlignment.Left + HorizontalContentAlignment = HorizontalAlignment.Left, + Content = GetContent(dataItem), + NavigateUri = GetNavigateUri(dataItem) }; - // Bind NavigateUri to the main Binding property - hyperlinkButton.SetBinding(HyperlinkButton.NavigateUriProperty, Binding); + return hyperlinkButton; + } + + /// + /// Gets the NavigateUri for the HyperlinkButton based on the cell content or binding. + /// + /// The data item associated with the cell. + /// The NavigateUri for the HyperlinkButton. + protected virtual Uri? GetNavigateUri(object? dataItem) + { + var cellContent = GetCellContent(dataItem); - // Bind Content to ContentBinding if set, otherwise use the NavigateUri - var contentBinding = ContentBinding ?? Binding; - hyperlinkButton.SetBinding(ContentControl.ContentProperty, contentBinding); + if (cellContent is Uri uri) + { + return uri; + } - return hyperlinkButton; + if (cellContent is string str) + { + return new Uri(str, UriKind.RelativeOrAbsolute); + } + + return default; + } + + /// + /// Gets the content for the HyperlinkButton based on the ContentBinding or falls back to the cell content. + /// + /// The data item associated with the cell. + /// The content for the HyperlinkButton. + protected virtual object? GetContent(object? dataItem) + { + if (TableView?.CellValueProvider is { } provider && + provider.TryGetContentBindingValue(ContentBinding?.Path?.Path, dataItem, out var value)) + { + return value; + } + + return GetCellContent(dataItem); + } + + /// + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not HyperlinkButton hyperlinkButton) + base.RefreshElement(cell, dataItem); + else + { + hyperlinkButton.Content = GetContent(dataItem); + hyperlinkButton.NavigateUri = GetNavigateUri(dataItem); + } } /// diff --git a/src/Columns/TableViewNumberColumn.cs b/src/Columns/TableViewNumberColumn.cs index 9c93fab3..a89412aa 100644 --- a/src/Columns/TableViewNumberColumn.cs +++ b/src/Columns/TableViewNumberColumn.cs @@ -1,5 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System; +using System.Globalization; using WinUI.TableView.Extensions; namespace WinUI.TableView; @@ -26,8 +28,8 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat { TextAlignment = TextAlignment.Right, Margin = new Thickness(12, 0, 12, 0), + Text = GetCellContent(dataItem)?.ToString() }; - textBlock.SetBinding(TextBlock.TextProperty, Binding); return textBlock; } @@ -40,23 +42,34 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat /// A NumberBox element. public override FrameworkElement GenerateEditingElement(TableViewCell cell, object? dataItem) { - var numberBox = new NumberBox(); - numberBox.SetBinding(NumberBox.ValueProperty, Binding); -#if !WINDOWS - numberBox.DataContext = dataItem; -#endif - return numberBox; + var value = GetCellContent(dataItem) switch + { + double d => d, + IConvertible c => c.ToDouble(CultureInfo.InvariantCulture), + _ => 0d + }; + + return new NumberBox { Value = value }; + } + + /// + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not TextBlock textBlock) + base.RefreshElement(cell, dataItem); + else + textBlock.Text = GetCellContent(dataItem)?.ToString(); } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is NumberBox numberBox) { return numberBox.Value; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// @@ -68,8 +81,7 @@ protected internal override void EndCellEditing(TableViewCell cell, object? data { numberBox.UpdateValue(); - var bindingExpression = numberBox.GetBindingExpression(NumberBox.ValueProperty); - bindingExpression?.UpdateSource(); + TrySetBindingValue(dataItem, numberBox.Value); } } } diff --git a/src/Columns/TableViewTextColumn.cs b/src/Columns/TableViewTextColumn.cs index dca276ea..e6b09732 100644 --- a/src/Columns/TableViewTextColumn.cs +++ b/src/Columns/TableViewTextColumn.cs @@ -1,5 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System; +using WinUI.TableView.Helpers; namespace WinUI.TableView; @@ -24,8 +26,9 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat var textBlock = new TextBlock { Margin = new Thickness(12, 0, 12, 0), + Text = GetCellContent(dataItem)?.ToString() }; - textBlock.SetBinding(TextBlock.TextProperty, Binding); + return textBlock; } @@ -37,16 +40,20 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat /// A TextBox element. public override FrameworkElement GenerateEditingElement(TableViewCell cell, object? dataItem) { - var textBox = new TextBox(); - textBox.SetBinding(TextBox.TextProperty, Binding); -#if !WINDOWS - textBox.DataContext = dataItem; -#endif - return textBox; + return new TextBox { Text = GetCellContent(dataItem)?.ToString() }; } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not TextBlock textBlock) + base.RefreshElement(cell, dataItem); + else + textBlock.Text = GetCellContent(dataItem)?.ToString(); + } + + /// + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is TextBox textBox) { @@ -54,19 +61,15 @@ public override FrameworkElement GenerateEditingElement(TableViewCell cell, obje return textBox.Text; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// protected internal override void EndCellEditing(TableViewCell cell, object? dataItem, TableViewEditAction editAction, object? uneditedValue) { - if (cell.Content is TextBox textBox) + if (cell.Content is TextBox textBox && editAction == TableViewEditAction.Commit) { - if (editAction == TableViewEditAction.Commit) - { - var bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty); - bindingExpression?.UpdateSource(); - } + TrySetBindingValue(dataItem, textBox.Text); } } } diff --git a/src/Columns/TableViewTimeColumn.cs b/src/Columns/TableViewTimeColumn.cs index bf5ef184..fd24771a 100644 --- a/src/Columns/TableViewTimeColumn.cs +++ b/src/Columns/TableViewTimeColumn.cs @@ -40,12 +40,8 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat Margin = new Thickness(12, 0, 12, 0), }; - textBlock.SetBinding(DateTimeFormatHelper.ValueProperty, Binding); - textBlock.SetBinding(DateTimeFormatHelper.FormatProperty, new Binding - { - Path = new PropertyPath(nameof(ClockIdentifier)), - Source = this - }); + DateTimeFormatHelper.SetValue(textBlock, GetCellContent(dataItem)); + DateTimeFormatHelper.SetFormat(textBlock, ClockIdentifier); return textBlock; } @@ -58,30 +54,46 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat /// A TableViewTimePicker element. public override FrameworkElement GenerateEditingElement(TableViewCell cell, object? dataItem) { + var time = GetCellContent(dataItem) switch + { + TimeOnly timeOnly => timeOnly.ToTimeSpan(), + TimeSpan timeSpan => timeSpan, + DateTime dateTime => dateTime.TimeOfDay, + DateTimeOffset dateTimeOffset => dateTimeOffset.TimeOfDay, + _ => default + }; + var timePicker = new TableViewTimePicker { ClockIdentifier = ClockIdentifier, MinuteIncrement = MinuteIncrement, PlaceholderText = PlaceholderText ?? TableViewLocalizedStrings.TimePickerPlaceholder, - SourceType = GetSourcePropertyType(dataItem), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, + SelectedTime = time }; - timePicker.SetBinding(TableViewTimePicker.SelectedTimeProperty, Binding); - return timePicker; } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + public override void RefreshElement(TableViewCell cell, object? dataItem) + { + if (cell.Content is not TextBlock textBlock) + base.RefreshElement(cell, dataItem); + else + DateTimeFormatHelper.SetValue(textBlock, GetCellContent(dataItem)); + } + + /// + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is TableViewTimePicker timePicker) { return timePicker.SelectedTime; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// @@ -91,8 +103,7 @@ protected internal override void EndCellEditing(TableViewCell cell, object? data { if (editAction == TableViewEditAction.Commit) { - var bindingExpression = timePicker.GetBindingExpression(TimePicker.SelectedTimeProperty); - bindingExpression?.UpdateSource(); + TrySetBindingValue(dataItem, timePicker.Time); } } } @@ -167,6 +178,4 @@ public string? PlaceholderText /// Identifies the PlaceholderText dependency property. /// public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(TableViewTimeColumn), new PropertyMetadata(null)); - - } diff --git a/src/Columns/TableViewToggleSwitchColumn.cs b/src/Columns/TableViewToggleSwitchColumn.cs index 60213979..5faa47ab 100644 --- a/src/Columns/TableViewToggleSwitchColumn.cs +++ b/src/Columns/TableViewToggleSwitchColumn.cs @@ -34,10 +34,9 @@ public override FrameworkElement GenerateElement(TableViewCell cell, object? dat OnContent = OnContent, OffContent = OffContent, UseSystemFocusVisuals = false, - Margin = new Thickness(12, 0, 12, 0) + Margin = new Thickness(12, 0, 12, 0), + IsOn = GetCellContent(dataItem) as bool? ?? false }; - - toggleSwitch.SetBinding(ToggleSwitch.IsOnProperty, Binding); UpdateToggleButtonState(toggleSwitch); return toggleSwitch; @@ -59,14 +58,14 @@ public override void UpdateElementState(TableViewCell cell, object? dataItem) } /// - protected internal override object? PrepareCellForEdit(TableViewCell cell, RoutedEventArgs routedEvent) + protected internal override object? PrepareCellForEdit(TableViewCell cell, object? dataItem, RoutedEventArgs routedEvent) { if (cell.Content is ToggleSwitch toggleSwitch) { return toggleSwitch.IsOn; } - return base.PrepareCellForEdit(cell, routedEvent); + return base.PrepareCellForEdit(cell, dataItem, routedEvent); } /// @@ -76,8 +75,7 @@ protected internal override void EndCellEditing(TableViewCell cell, object? data { if (editAction == TableViewEditAction.Commit) { - var bindingExpression = toggleSwitch.GetBindingExpression(ToggleSwitch.IsOnProperty); - bindingExpression?.UpdateSource(); + TrySetBindingValue(dataItem, toggleSwitch.IsOn); } } } diff --git a/src/Controls/TableViewDatePicker.cs b/src/Controls/TableViewDatePicker.cs deleted file mode 100644 index d264c581..00000000 --- a/src/Controls/TableViewDatePicker.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using System; -using WinUI.TableView.Extensions; - -namespace WinUI.TableView.Controls; - -/// -/// Represents a date editing element for the TableViewDateColumn. -/// -public partial class TableViewDatePicker : CalendarDatePicker -{ - private bool _deferUpdate; - - /// - /// Initializes a new instance of the TableViewDatePicker class. - /// - public TableViewDatePicker() - { - DateChanged += OnDateChanged; - } - - /// - /// Handles the DateChanged event. - /// - private void OnDateChanged(CalendarDatePicker sender, CalendarDatePickerDateChangedEventArgs args) - { - if (_deferUpdate) return; - - _deferUpdate = true; - - if (Date is null) - { - SelectedDate = null; - } - else if (SourceType.IsDateOnly()) - { - SelectedDate = DateOnly.FromDateTime(Date.Value.DateTime); - } - else if (SourceType.IsDateTime()) - { - var newDate = Date.Value.DateTime; - var selectedDate = (DateTime?)SelectedDate ?? DateTime.Now; - SelectedDate = new DateTime(newDate.Year, newDate.Month, newDate.Day, - selectedDate.Hour, selectedDate.Minute, selectedDate.Second); - } - else if (SourceType.IsDateTimeOffset()) - { - var selectedDate = (DateTimeOffset?)SelectedDate ?? DateTimeOffset.Now; - var newDate = Date.Value; - SelectedDate = new DateTimeOffset(newDate.Year, newDate.Month, newDate.Day, - selectedDate.Hour, selectedDate.Minute, selectedDate.Second, selectedDate.Offset); - } - - _deferUpdate = false; - } - - /// - /// Handles changes to the SelectedDate property. - /// - private static void OnSelectedDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableViewDatePicker datePicker && !datePicker._deferUpdate) - { - datePicker._deferUpdate = true; - datePicker.Date = e.NewValue switch - { - DateOnly dateOnly => dateOnly.ToDateTimeOffset(), - DateTime dateTime => dateTime.ToDateTimeOffset(), - DateTimeOffset dateTimeOffset => dateTimeOffset, - _ => throw new FormatException() - }; - datePicker.SourceType ??= e.NewValue?.GetType(); - datePicker._deferUpdate = false; - } - } - - /// - /// Gets or sets the source type of the date picker. - /// This value could be DateOnly, DateTime, or DateTimeOffset. - /// - internal Type? SourceType { get; set; } - - /// - /// Gets or sets the selected date. - /// - public object? SelectedDate - { - get => GetValue(SelectedDateProperty); - set => SetValue(SelectedDateProperty, value); - } - - /// - /// Identifies the SelectedDate dependency property. - /// - public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register(nameof(SelectedDate), typeof(object), typeof(TableViewDatePicker), new PropertyMetadata(default, OnSelectedDateChanged)); -} \ No newline at end of file diff --git a/src/Controls/TableViewFilterItemsControl.xaml b/src/Controls/TableViewFilterItemsControl.xaml new file mode 100644 index 00000000..76b3b6fa --- /dev/null +++ b/src/Controls/TableViewFilterItemsControl.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controls/TableViewFilterItemsControl.xaml.cs b/src/Controls/TableViewFilterItemsControl.xaml.cs new file mode 100644 index 00000000..ed4446b2 --- /dev/null +++ b/src/Controls/TableViewFilterItemsControl.xaml.cs @@ -0,0 +1,185 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Windows.System; + +namespace WinUI.TableView.Controls; + +/// +/// Represents the control that displays filter items in the filter flyout of a TableViewColumnHeader. +/// +public partial class TableViewFilterItemsControl : UserControl +{ + private bool _canSetState = true; + private ICollection? _filterItems; + + /// + /// Initializes a new instance of the class. + /// + public TableViewFilterItemsControl() + { + InitializeComponent(); + } + + /// + /// Initializes the state of the . + /// + internal async void Initialize() + { + FilterItems = TableView?.FilterHandler?.GetFilterItems(ColumnHeader?.Column!, null).ToList(); + + if (searchBox is not null) + { + await Task.Delay(100); + await FocusManager.TryFocusAsync(searchBox, FocusState.Programmatic); + } + + if (filterItemsList is not null && filterItemsList.Items.Count > 0) + { + filterItemsList.ScrollIntoView(filterItemsList.Items[0]); + } + } + + /// + /// Clears the search box text. + /// + internal void ClearSearchBox() + { + if (searchBox is not null) + { + searchBox.Text = string.Empty; + } + } + + private void OnSearchBoxTextChanged(object sender, TextChangedEventArgs e) + { + FilterItems = TableView?.FilterHandler?.GetFilterItems(ColumnHeader?.Column!, searchBox!.Text); + } + + /// + /// Handles the KeyDown or PreviewKeyDown event for the searchBox. + /// + private void OnSearchBoxKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter && searchBox?.Text.Length > 0) + { + ColumnHeader?.ExecuteOkCommand(); + + e.Handled = true; + } + } + + /// + /// Handles the Checked and Unchecked event for the selectAllCheckBox. + /// + private void OnSelectAllCheckBoxCheckChanged(object sender, RoutedEventArgs e) + { + SetFilterItemsState(selectAllCheckBox.IsChecked is true); + } + + /// + /// Sets the state of the select all checkbox. + /// + internal void SetSelectAllCheckBoxState() + { + if (selectAllCheckBox is null || !_canSetState) + { + return; + } + + + selectAllCheckBox.IsChecked = _filterItems?.All(x => x.IsSelected) ?? false ? true + : _filterItems?.All(x => !x.IsSelected) ?? false ? false + : null; + } + + /// + /// Sets the state of the filter items. + /// + /// The state to set. + internal void SetFilterItemsState(bool isSelected) + { + _canSetState = false; + + foreach (var item in filterItemsList.Items.OfType()) + { + item.IsSelected = isSelected; + } + + _canSetState = true; + } + + /// + /// Attaches property changed handlers to the filter items. + /// + private void AttachPropertyChangedHandlers() + { + if (_filterItems?.Count > 0) + { + foreach (var item in _filterItems) + { + item.PropertyChanged += OnFilterItemPropertyChanged; + } + } + } + + /// + /// Detaches property changed handlers from the filter items. + /// + private void DetachPropertyChangedHandlers() + { + if (_filterItems?.Count > 0) + { + foreach (var item in _filterItems) + { + item.PropertyChanged -= OnFilterItemPropertyChanged; + } + } + } + + /// + /// Handles the PropertyChanged event for filter items. + /// + private void OnFilterItemPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + SetSelectAllCheckBoxState(); + } + + /// + /// Gets a value indicating whether to apply the filter based on the control state. + /// + internal bool ShouldApplyFilter => selectAllCheckBox.IsChecked is false || !string.IsNullOrEmpty(searchBox.Text); + + /// + /// Gets or sets the filter items for the control. + /// + internal ICollection? FilterItems + { + get => _filterItems; + set + { + if (_filterItems == value) return; + + DetachPropertyChangedHandlers(); + _filterItems = value; + filterItemsList.ItemsSource = _filterItems; + AttachPropertyChangedHandlers(); + SetSelectAllCheckBoxState(); + } + } + + /// + /// Gets or sets the column header associated with the filter items control. + /// + public TableViewColumnHeader? ColumnHeader { get; internal set; } + + /// + /// Gets or sets the TableView associated with the filter items control. + /// + public TableView? TableView { get; internal set; } +} diff --git a/src/Controls/TableViewTimePicker.cs b/src/Controls/TableViewTimePicker.cs index ac56734f..dca5654f 100644 --- a/src/Controls/TableViewTimePicker.cs +++ b/src/Controls/TableViewTimePicker.cs @@ -12,168 +12,43 @@ namespace WinUI.TableView.Controls; /// /// Represents a time editing element for the TableViewTimeColumn. /// -public partial class TableViewTimePicker : Control +public partial class TableViewTimePicker : TimePicker { - private TextBlock? _timeText; - private readonly TimePickerFlyout _flyout; - /// /// Initializes a new instance of the TableViewTimePicker class. /// public TableViewTimePicker() { DefaultStyleKey = typeof(TableViewTimePicker); - - _flyout = new() { Placement = FlyoutPlacementMode.Bottom }; - _flyout.TimePicked += OnTimePicked; - - ClockIdentifier = _flyout.ClockIdentifier; - } - - /// - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - _timeText = GetTemplateChild("TimeText") as TextBlock; - - UpdateTimeText(); - } - - /// - protected override void OnPointerPressed(PointerRoutedEventArgs e) - { - base.OnPointerPressed(e); - - ShowFlyout(); - } - - /// - protected override void OnKeyDown(KeyRoutedEventArgs e) - { - base.OnKeyDown(e); - - if (e.Key is VirtualKey.Space) - { - ShowFlyout(); - } - } - - /// - /// Shows the time picker flyout. - /// - private void ShowFlyout() - { - _flyout.Time = SelectedTime switch - { - TimeSpan timeSpan => timeSpan, - TimeOnly timeOnly => timeOnly.ToTimeSpan(), - DateTime dateTime => dateTime.TimeOfDay, - DateTimeOffset dateTimeOffset => dateTimeOffset.TimeOfDay, - _ => _flyout.Time - }; - - _flyout.ClockIdentifier = ClockIdentifier; - _flyout.MinuteIncrement = MinuteIncrement; - _flyout.ShowAt(this); - } - - /// - /// Handles the TimePicked event of the flyout. - /// - private void OnTimePicked(TimePickerFlyout sender, TimePickedEventArgs args) - { - var oldTime = SelectedTime is null ? TimeSpan.Zero : args.OldTime; - - if (SourceType.IsTimeSpan()) - { - SelectedTime = args.NewTime; - } - else if (SourceType.IsTimeOnly()) - { - SelectedTime = TimeOnly.FromTimeSpan(args.NewTime); - } - else if (SourceType.IsDateTime()) - { - var dateTime = (DateTime?)SelectedTime ?? DateTime.Today; - SelectedTime = dateTime.Subtract(oldTime).Add(args.NewTime); - } - else if (SourceType.IsDateTimeOffset()) - { - var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Today); - var dateTimeOffset = (DateTimeOffset?)SelectedTime ?? new DateTimeOffset(DateTime.Today, offset); - SelectedTime = dateTimeOffset.Subtract(oldTime).Add(args.NewTime); - } - } - - /// - /// Updates the text displayed in the time picker. - /// - private void UpdateTimeText() - { - if (_timeText is null) return; - - var formatter = DateTimeFormatHelper.GetDateTimeFormatter("shorttime", ClockIdentifier); - - _timeText.Text = SelectedTime switch - { - TimeSpan timeSpan => formatter.Format(timeSpan.ToDateTimeOffset()), - TimeOnly timeOnly => formatter.Format(timeOnly.ToDateTimeOffset()), - DateTime dateTime => formatter.Format(dateTime.ToDateTimeOffset()), - DateTimeOffset dateTimeOffset => formatter.Format(dateTimeOffset), - null => PlaceholderText, - _ => throw new FormatException() - }; - } - - /// - /// Handles changes to the SelectedTime property. - /// - private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableViewTimePicker timePicker) - { - timePicker.UpdateTimeText(); - timePicker.SourceType ??= e.NewValue?.GetType(); - } - } - - /// - /// Handles changes to the PlaceholderText property. - /// - private static void OnPlaceHolderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableViewTimePicker timePicker) - { - timePicker.UpdateTimeText(); - } } - /// - /// Handles changes to the ClockIdentifier property. - /// - private static void OnClockIdentifierChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableViewTimePicker timePicker) - { - timePicker.UpdateTimeText(); - } - } - - /// - /// Gets or sets the source type of the time picker. - /// The value could be TimeSpan, TimeOnly, DateTime, or DateTimeOffset. - /// - internal Type? SourceType { get; set; } - - /// - /// Gets or sets the selected time. - /// - public object? SelectedTime - { - get => GetValue(SelectedTimeProperty); - set => SetValue(SelectedTimeProperty, value); - } + ///// + ///// Handles the TimePicked event of the flyout. + ///// + //private void OnTimePicked(TimePickerFlyout sender, TimePickedEventArgs args) + //{ + // var oldTime = SelectedTime is null ? TimeSpan.Zero : args.OldTime; + + // if (SourceType.IsTimeSpan()) + // { + // SelectedTime = args.NewTime; + // } + // else if (SourceType.IsTimeOnly()) + // { + // SelectedTime = TimeOnly.FromTimeSpan(args.NewTime); + // } + // else if (SourceType.IsDateTime()) + // { + // var dateTime = (DateTime?)SelectedTime ?? DateTime.Today; + // SelectedTime = dateTime.Subtract(oldTime).Add(args.NewTime); + // } + // else if (SourceType.IsDateTimeOffset()) + // { + // var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Today); + // var dateTimeOffset = (DateTimeOffset?)SelectedTime ?? new DateTimeOffset(DateTime.Today, offset); + // SelectedTime = dateTimeOffset.Subtract(oldTime).Add(args.NewTime); + // } + //} /// /// Gets or sets the placeholder text for the time picker. @@ -184,41 +59,8 @@ public string? PlaceholderText set => SetValue(PlaceholderTextProperty, value); } - /// - /// Gets or sets the clock identifier for the time picker. - /// - public string ClockIdentifier - { - get => (string)GetValue(ClockIdentifierProperty); - set => SetValue(ClockIdentifierProperty, value); - } - - /// - /// Gets or sets the minute increment for the time picker. - /// - public int MinuteIncrement - { - get => (int)GetValue(MinuteIncrementProperty); - set => SetValue(MinuteIncrementProperty, value); - } - - /// - /// Identifies the MinuteIncrement dependency property. - /// - public static readonly DependencyProperty MinuteIncrementProperty = DependencyProperty.Register(nameof(MinuteIncrement), typeof(int), typeof(TableViewTimePicker), new PropertyMetadata(1)); - - /// - /// Identifies the SelectedTime dependency property. - /// - public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register(nameof(SelectedTime), typeof(object), typeof(TableViewTimePicker), new PropertyMetadata(default, OnSelectedTimeChanged)); - /// /// Identifies the PlaceholderText dependency property. /// - public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(TableViewTimePicker), new PropertyMetadata("pick a time", OnPlaceHolderTextChanged)); - - /// - /// Identifies the ClockIdentifier dependency property. - /// - public static readonly DependencyProperty ClockIdentifierProperty = DependencyProperty.Register(nameof(ClockIdentifier), typeof(string), typeof(TableViewTimePicker), new PropertyMetadata(default, OnClockIdentifierChanged)); + public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(TableViewTimePicker), new PropertyMetadata("pick a time")); } \ No newline at end of file diff --git a/src/Extensions/DependencyObjectExtensions.cs b/src/Extensions/DependencyObjectExtensions.cs index af7300b5..ac799260 100644 --- a/src/Extensions/DependencyObjectExtensions.cs +++ b/src/Extensions/DependencyObjectExtensions.cs @@ -58,7 +58,7 @@ internal static IEnumerable FindDescendants(this DependencyObj /// The type of ascendant to find. /// The element to start searching from. /// The first matching ascendant, or null if none found. - internal static T? FindAscendant(this DependencyObject element) where T : DependencyObject + internal static T? FindAscendant(this DependencyObject element) { foreach (var ascendant in element.FindAscendants()) { diff --git a/src/Helpers/BindingHelper.cs b/src/Helpers/BindingHelper.cs new file mode 100644 index 00000000..1c812ce8 --- /dev/null +++ b/src/Helpers/BindingHelper.cs @@ -0,0 +1,56 @@ +using Microsoft.UI.Xaml.Data; +using System; + +namespace WinUI.TableView.Helpers; + +/// +/// Provides helper methods for applying value converters to data bindings. +/// +internal static class BindingHelper +{ + /// + /// Converts the specified value using the binding's value converter, if one is present. + /// + /// If the binding does not specify a converter, the method returns the input value unchanged. + /// The binding whose converter will be used to convert the value. Can be null if no conversion is required. + /// The value to be converted using the binding's converter. + /// The target type to convert the value to. If null, defaults to object. + /// The converted value if a converter is present; otherwise, the original value. + public static object? ConvertValue(Binding? binding, object? value, Type? targetType = null) + { + if (binding?.Converter is { } converter) + { + targetType ??= typeof(object); + value = converter.Convert( + value, + targetType, + binding.ConverterParameter, + binding.ConverterLanguage); + } + + return value; + } + + /// + /// Converts back the specified value using the binding's value converter, if one is present. + /// + /// If the binding does not specify a converter, the method returns the input value unchanged. + /// The binding whose converter will be used to convert back the value. Can be null if no conversion is required. + /// The value to be converted using the binding's converter. + /// The target type to convert back the value to. If null, defaults to object. + /// The converted value if a converter is present; otherwise, the original value. + public static object? ConvertBackValue(Binding? binding, object? value, Type? targetType = null) + { + if (binding?.Converter is { } converter) + { + targetType ??= typeof(object); + value = converter.ConvertBack( + value, + targetType, + binding.ConverterParameter, + binding.ConverterLanguage); + } + + return value; + } +} \ No newline at end of file diff --git a/src/Helpers/DateTimeFormatHelper.cs b/src/Helpers/DateTimeFormatHelper.cs index 81bf1e84..0872c103 100644 --- a/src/Helpers/DateTimeFormatHelper.cs +++ b/src/Helpers/DateTimeFormatHelper.cs @@ -13,7 +13,7 @@ namespace WinUI.TableView.Helpers; /// /// Provides helper methods for formatting Date and Time values. /// -internal static class DateTimeFormatHelper +public static class DateTimeFormatHelper { private const string _12HourClock = "12HourClock"; private const string _24HourClock = "24HourClock"; @@ -44,7 +44,7 @@ private static void SetFormattedText(TextBlock textBlock) textBlock.Text = formatter.Format(dateTimeOffset); } - else if (value is not null) + else if (value is not null && !string.IsNullOrEmpty(format)) { var formatter = GetDateTimeFormatter(format); @@ -118,7 +118,7 @@ private static void OnFormatChanged(DependencyObject d, DependencyPropertyChange /// /// Gets the value of the Value attached property. /// - public static object GetValue(DependencyObject obj) + public static object? GetValue(DependencyObject obj) { return obj.GetValue(ValueProperty); } @@ -126,7 +126,7 @@ public static object GetValue(DependencyObject obj) /// /// Sets the value of the Value attached property. /// - public static void SetValue(DependencyObject obj, object value) + public static void SetValue(DependencyObject obj, object? value) { obj.SetValue(ValueProperty, value); } diff --git a/src/ICellValueProvider.cs b/src/ICellValueProvider.cs new file mode 100644 index 00000000..61a5ba13 --- /dev/null +++ b/src/ICellValueProvider.cs @@ -0,0 +1,62 @@ +namespace WinUI.TableView; + +/// +/// Provides value resolution for generated TableView sort, display, and clipboard paths. +/// +public interface ICellValueProvider +{ + /// + /// Tries to resolve a cell display value for the specified . + /// + /// The member path to resolve. + /// The row item instance. + /// When this method returns, contains the resolved value if successful. + /// when a value was resolved; otherwise . + bool TryGetBindingValue(string? path, object? item, out object? value); + + /// + /// Tries to set a cell value for the specified . + /// + /// The member path to resolve. + /// The row item instance. + /// The value to set. + /// when a value was set; otherwise . + bool TrySetBindingValue(string? path, object? item, object? value); + + /// + /// Tries to resolve a sort value for the specified . + /// + /// The member path to resolve. + /// The row item instance. + /// When this method returns, contains the resolved value if successful. + /// when a value was resolved; otherwise . + bool TryGetSortMemberValue(string? path, object? item, out object? value); + + /// + /// Tries to resolve clipboard content for the specified . + /// + /// The member path to resolve. + /// The row item instance. + /// When this method returns, contains the resolved value if successful. + /// when a value was resolved; otherwise . + bool TryGetClipboardContentBindingValue(string? path, object? item, out object? value); + + /// + /// Tries to resolve a content value for the specified . + /// + /// The content binding path to resolve. + /// The row item instance. + /// When this method returns, contains the resolved value if successful. + /// when a value was resolved; otherwise . + bool TryGetContentBindingValue(string? path, object? item, out object? value); + + /// + /// Tries to resolve a display member value for the combo box column when the column's is used to specified. + /// + /// The member path to resolve. + /// The row item instance. + /// When this method returns, contains the resolved value if successful. + /// when a value was resolved; otherwise . + bool TryGetDisplayMemberValue(string? path, object? item, out object? value); +} + diff --git a/src/ITableViewConnector.cs b/src/ITableViewConnector.cs new file mode 100644 index 00000000..20e5fb6a --- /dev/null +++ b/src/ITableViewConnector.cs @@ -0,0 +1,18 @@ +namespace WinUI.TableView; + +/// +/// Defines a contract for connecting generated TableView helpers to named TableView instances. +/// +public interface ITableViewConnector +{ + /// + /// Connects a generated provider to the specified instance. + /// + /// The target to connect. + void ConnectTableView(TableView tableView); + + /// + /// Connects generated providers to their corresponding TableView controls. + /// + void ConnectTableViews(); +} diff --git a/src/ItemsSource/ColumnSortDescription.cs b/src/ItemsSource/ColumnSortDescription.cs index 02692020..4bd3dead 100644 --- a/src/ItemsSource/ColumnSortDescription.cs +++ b/src/ItemsSource/ColumnSortDescription.cs @@ -19,13 +19,25 @@ public ColumnSortDescription(TableViewColumn column, Column = column; } + /// + /// Gets the sort value for the specified item, preferring the TableView member value provider when available. + /// + /// The item to extract a sort value from. + /// The resolved sort value, or when no value is available. public override object? GetPropertyValue(object? item) { // Use reflection-based property access when SortMemberPath is explicitly provided; otherwise, fall back to column cell content. if (!string.IsNullOrEmpty(Column.SortMemberPath)) { + if (Column.TableView?.CellValueProvider is { } provider + && provider.TryGetSortMemberValue(Column.SortMemberPath, item, out var value)) + { + return value; + } + return base.GetPropertyValue(item); } + return Column.GetCellContent(item); } diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index d90e7d26..29153f9f 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -60,7 +60,7 @@ public partial class TableView /// /// Identifies the ShowExportOptions dependency property. /// - public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false)); + public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false, OnShowExportOptionsChanged)); /// /// Identifies the AutoGenerateColumns dependency property. @@ -371,6 +371,11 @@ public bool ShowFilterItemsCount /// public IColumnFilterHandler FilterHandler { get; set; } + /// + /// Gets or sets the provider used to resolve values. + /// + public ICellValueProvider? CellValueProvider { get; set; } + /// /// Gets a value indicating whether the TableView items are filtered. /// @@ -827,6 +832,17 @@ private static void OnSelectionModeChanged(DependencyObject d, DependencyPropert } } + /// + /// Handles changes to the ShowExportOptions property. + /// + private static void OnShowExportOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + tableView._headerRow?.SetExportOptionsVisibility(); + } + } + /// /// Handles changes to the AutoGenerateColumns property. /// diff --git a/src/TableView.cs b/src/TableView.cs index e21a6d3e..85b406c1 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -1,3 +1,4 @@ +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; @@ -13,6 +14,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; using Windows.Storage; @@ -99,7 +101,7 @@ private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) } /// - protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + protected override void PrepareContainerForItemOverride(DependencyObject element, object? item) { base.PrepareContainerForItemOverride(element, item); @@ -107,6 +109,7 @@ protected override void PrepareContainerForItemOverride(DependencyObject element { if (element is TableViewRow row) { + row.EnsureCells(item); row.EnsureCellsStyle(default, item); row.ApplyCellsSelectionState(); row.RowPresenter?.ApplyDetailsPaneState(item); @@ -232,6 +235,12 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c } } + /// + /// Ends cell editing by raising ending/ended events and applying the requested edit action. + /// + /// The action to apply when ending edit mode. + /// The cell currently being edited. + /// if editing ended; otherwise when canceled by event handlers. internal bool EndCellEditing(TableViewEditAction editAction, TableViewCell cell) { var editingElement = cell.Content as FrameworkElement; @@ -292,6 +301,12 @@ protected async override void OnApplyTemplate() } SetHeadersVisibility(); + + if (CellValueProvider is null) + { + // Connect this TableView instance to avoid re-connecting unrelated TableViews. + this.FindAscendant()?.ConnectTableView(this); + } } /// @@ -1413,7 +1428,7 @@ internal void EnsureCells() { foreach (var row in _rows) { - row.EnsureCells(); + row.EnsureCells(row?.Content); } } #endif @@ -1493,6 +1508,10 @@ internal void UpdateCornerButtonState() }); } + /// + /// Updates the internal editing state and refreshes dependent visual states. + /// + /// The new editing-state value. internal void SetIsEditing(bool value) { if (IsEditing == value) diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs index 758c3b14..b3201993 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -414,7 +414,7 @@ private void OnEditingElementLoaded(object sender, RoutedEventArgs e) _editingArgs ??= new RoutedEventArgs(); var args = new TableViewPreparingCellForEditEventArgs(this, Row?.Content, Column!, editingElement, _editingArgs); - _uneditedValue = Column?.PrepareCellForEdit(this, _editingArgs); + _uneditedValue = Column?.PrepareCellForEdit(this, Row?.Content, _editingArgs); TableView?.OnPreparingCellForEdit(args); } } @@ -473,9 +473,9 @@ internal void SetElement() /// /// Refreshes the element for the cell. /// - internal void RefreshElement() + internal void RefreshElement(object? dataItem) { - Column?.RefreshElement(this, Row?.Content); + Column?.RefreshElement(this, dataItem); } /// diff --git a/src/TableViewColumnHeader.OptionComamnds.cs b/src/TableViewColumnHeader.OptionComamnds.cs new file mode 100644 index 00000000..a90976e9 --- /dev/null +++ b/src/TableViewColumnHeader.OptionComamnds.cs @@ -0,0 +1,70 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using WinUI.TableView.Extensions; +using SD = WinUI.TableView.SortDirection; + +namespace WinUI.TableView; + +partial class TableViewColumnHeader +{ + private readonly StandardUICommand _sortAscendingCommand = new() { Label = TableViewLocalizedStrings.SortAscending }; + private readonly StandardUICommand _sortDescendingCommand = new() { Label = TableViewLocalizedStrings.SortDescending }; + private readonly StandardUICommand _clearSortingCommand = new() { Label = TableViewLocalizedStrings.ClearSorting }; + private readonly StandardUICommand _clearFilterCommand = new() { Label = TableViewLocalizedStrings.ClearFilter }; + private readonly StandardUICommand _okCommand = new() { Label = TableViewLocalizedStrings.Ok }; + private readonly StandardUICommand _cancelCommand = new() { Label = TableViewLocalizedStrings.Cancel }; + + /// + /// Sets commands to option menu items. + /// + private void SetOptionCommands() + { + InitializeCommands(); + + if (GetTemplateChild("SortAscendingMenuItem") is MenuFlyoutItem sortAscendingMenuItem) + sortAscendingMenuItem.Command = _sortAscendingCommand; + if (GetTemplateChild("SortDescendingMenuItem") is MenuFlyoutItem sortDescendingMenuItem) + sortDescendingMenuItem.Command = _sortDescendingCommand; + if (GetTemplateChild("ClearSortingMenuItem") is MenuFlyoutItem clearSortingMenuItem) + clearSortingMenuItem.Command = _clearSortingCommand; + if (GetTemplateChild("ClearFilterMenuItem") is MenuFlyoutItem clearFilterMenuItem) + clearFilterMenuItem.Command = _clearFilterCommand; + if (GetTemplateChild("ActionButtonsMenuItem") is MenuFlyoutItem actionButtonsMenuItem) + { + actionButtonsMenuItem.ApplyTemplate(); + + if (actionButtonsMenuItem.FindDescendant + + + + + \ No newline at end of file diff --git a/src/WinUI.TableView.csproj b/src/WinUI.TableView.csproj index bcdf89da..aa57be38 100644 --- a/src/WinUI.TableView.csproj +++ b/src/WinUI.TableView.csproj @@ -6,7 +6,7 @@ net10.0; 12 - enable + enable true net8.0-windows10.0.19041.0; @@ -31,12 +31,12 @@ - + - + @@ -45,11 +45,19 @@ - - - TableView.cs - - + + + TableView.cs + + + + + + + + + + WinUI.TableView