From 329fd11bf9b4e072bb6e709a1e83ff0a12fcefcd Mon Sep 17 00:00:00 2001 From: Iakov Lilo Date: Thu, 10 Nov 2022 10:44:45 +1100 Subject: [PATCH 1/8] chore: add a project for Fast Controls --- ...OpenSilver.ControlsKit.FastControls.csproj | 8 ++++++ src/OpenSilver.ControlsKit.sln | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/FastControls/OpenSilver.ControlsKit.FastControls.csproj create mode 100644 src/OpenSilver.ControlsKit.sln diff --git a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj new file mode 100644 index 0000000..f2c0c84 --- /dev/null +++ b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.0 + OpenSilver.ControlsKit + + + diff --git a/src/OpenSilver.ControlsKit.sln b/src/OpenSilver.ControlsKit.sln new file mode 100644 index 0000000..72ddc7d --- /dev/null +++ b/src/OpenSilver.ControlsKit.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSilver.ControlsKit.FastControls", "FastControls\OpenSilver.ControlsKit.FastControls.csproj", "{A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3095C2CC-DAE8-4223-BF01-AFDDF80767AD} + EndGlobalSection +EndGlobal From b65b84f3107dcf628d3aea18a6728806ed556ef8 Mon Sep 17 00:00:00 2001 From: Iakov Lilo Date: Thu, 10 Nov 2022 17:22:44 +1100 Subject: [PATCH 2/8] feat: add FastCheckBox component and example of usage --- src/.editorconfig | 127 +++++++++ .../FastControls.TestApp.Browser/App.cs | 42 +++ .../FastControls.TestApp.Browser.csproj | 26 ++ .../UnmarshalledJavaScriptExecutionHandler.cs | 27 ++ .../Pages/Index.cs | 26 ++ .../FastControls.TestApp.Browser/Program.cs | 31 +++ .../Properties/launchSettings.json | 29 ++ .../wwwroot/.gitignore | 2 + .../wwwroot/BlazorLoader.js | 25 ++ .../wwwroot/favicon.ico | Bin 0 -> 128052 bytes .../wwwroot/index.html | 21 ++ .../wwwroot/loading-indicator.css | 154 +++++++++++ .../wwwroot/loading-indicator.js | 28 ++ .../FastControls.TestApp.Simulator.csproj | 16 ++ .../FastControls.TestApp.Simulator/Startup.cs | 14 + .../FastControls.TestApp.sln | 37 +++ .../FastControls.TestApp/App.xaml | 11 + .../FastControls.TestApp/App.xaml.cs | 22 ++ .../FastControls.TestApp.csproj | 37 +++ .../FastControls.TestApp/MainPage.xaml | 32 +++ .../FastControls.TestApp/MainPage.xaml.cs | 60 ++++ .../Pages/FastCheckBoxPage.xaml | 10 + .../Pages/FastCheckBoxPage.xaml.cs | 18 ++ .../Properties/launchSettings.json | 9 + .../Registry/TestRegistry.cs | 17 ++ .../FastControls.TestApp/Registry/TreeItem.cs | 31 +++ src/FastControls/FastCheckBox.cs | 257 ++++++++++++++++++ ...OpenSilver.ControlsKit.FastControls.csproj | 4 + src/Nuget.Config | 6 + src/OpenSilver.ControlsKit.sln | 27 +- 30 files changed, 1145 insertions(+), 1 deletion(-) create mode 100644 src/.editorconfig create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/App.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/Interop/UnmarshalledJavaScriptExecutionHandler.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/Pages/Index.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/Program.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/Properties/launchSettings.json create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/.gitignore create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/BlazorLoader.js create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/favicon.ico create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/index.html create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.css create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.js create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj create mode 100644 src/FastControls.TestApp/FastControls.TestApp.Simulator/Startup.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp.sln create mode 100644 src/FastControls.TestApp/FastControls.TestApp/App.xaml create mode 100644 src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj create mode 100644 src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml create mode 100644 src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml create mode 100644 src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp/Properties/launchSettings.json create mode 100644 src/FastControls.TestApp/FastControls.TestApp/Registry/TestRegistry.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp/Registry/TreeItem.cs create mode 100644 src/FastControls/FastCheckBox.cs create mode 100644 src/Nuget.Config diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..1f3f583 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,127 @@ +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_space_around_binary_operators = before_and_after +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/App.cs b/src/FastControls.TestApp/FastControls.TestApp.Browser/App.cs new file mode 100644 index 0000000..7228a29 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/App.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.CompilerServices; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; + +namespace FastControls.TestApp.Browser +{ + public class App : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddAttribute(1, "AppAssembly", RuntimeHelpers.TypeCheck( + typeof(Program).Assembly + )); + builder.AddAttribute(2, "PreferExactMatches", RuntimeHelpers.TypeCheck( + true + )); + builder.AddAttribute(3, "Found", (RenderFragment)(routeData => builder2 => + { + builder2.OpenComponent(4); + builder2.AddAttribute(5, "RouteData", RuntimeHelpers.TypeCheck( + routeData + )); + builder2.CloseComponent(); + } + )); + builder.AddAttribute(7, "NotFound", (RenderFragment)(builder2 => + { + builder2.OpenComponent(8); + builder2.AddAttribute(9, "ChildContent", (RenderFragment)(builder3 => + { + builder3.AddMarkupContent(10, "

Sorry, there\'s nothing at this address.

"); + } + )); + builder2.CloseComponent(); + } + )); + builder.CloseComponent(); + } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj b/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj new file mode 100644 index 0000000..7f2be62 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + True + 4 + true + + + + + + + + + + + + True + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/Interop/UnmarshalledJavaScriptExecutionHandler.cs b/src/FastControls.TestApp/FastControls.TestApp.Browser/Interop/UnmarshalledJavaScriptExecutionHandler.cs new file mode 100644 index 0000000..ad64fd7 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Interop/UnmarshalledJavaScriptExecutionHandler.cs @@ -0,0 +1,27 @@ +using DotNetForHtml5; +using Microsoft.JSInterop; +using Microsoft.JSInterop.WebAssembly; + +namespace FastControls.TestApp.Browser.Interop +{ + public class UnmarshalledJavaScriptExecutionHandler : IJavaScriptExecutionHandler + { + private const string MethodName = "callJSUnmarshalled"; + private readonly WebAssemblyJSRuntime _runtime; + + public UnmarshalledJavaScriptExecutionHandler(IJSRuntime runtime) + { + _runtime = runtime as WebAssemblyJSRuntime; + } + + public void ExecuteJavaScript(string javaScriptToExecute) + { + _runtime.InvokeUnmarshalled(MethodName, javaScriptToExecute); + } + + public object ExecuteJavaScriptWithResult(string javaScriptToExecute) + { + return _runtime.InvokeUnmarshalled(MethodName, javaScriptToExecute); + } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/Pages/Index.cs b/src/FastControls.TestApp/FastControls.TestApp.Browser/Pages/Index.cs new file mode 100644 index 0000000..d297ed2 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Pages/Index.cs @@ -0,0 +1,26 @@ +using DotNetForHtml5; +using FastControls.TestApp.Browser.Interop; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.JSInterop; + +namespace FastControls.TestApp.Browser.Pages +{ + [Route("/")] + public class Index : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder __builder) + { + } + + protected override void OnInitialized() + { + base.OnInitialized(); + Cshtml5Initializer.Initialize(new UnmarshalledJavaScriptExecutionHandler(JSRuntime)); + Program.RunApplication(); + } + + [Inject] + private IJSRuntime JSRuntime { get; set; } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/Program.cs b/src/FastControls.TestApp/FastControls.TestApp.Browser/Program.cs new file mode 100644 index 0000000..981e810 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; + +namespace FastControls.TestApp.Browser +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + + builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + var host = builder.Build(); + await host.RunAsync(); + } + + public static void RunApplication() + { + Application.RunApplication(() => + { + var app = new FastControls.TestApp.App(); + }); + } + } +} diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/Properties/launchSettings.json b/src/FastControls.TestApp/FastControls.TestApp.Browser/Properties/launchSettings.json new file mode 100644 index 0000000..0db4f15 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55591/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "FastControls.TestApp.Browser": { + "commandName": "Project", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:55592/" + } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/.gitignore b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/.gitignore new file mode 100644 index 0000000..afa6524 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/.gitignore @@ -0,0 +1,2 @@ +libs +resources \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/BlazorLoader.js b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/BlazorLoader.js new file mode 100644 index 0000000..34b8ca3 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/BlazorLoader.js @@ -0,0 +1,25 @@ +var i = 0; +var allResourcesBeingLoaded = []; +Blazor.start({ // start manually with loadBootResource + loadBootResource: function (type, name, defaultUri, integrity) { + if (type == "dotnetjs") + return defaultUri; + + var fetchResources = fetch(defaultUri, { + cache: 'no-cache', + integrity: integrity, + headers: { 'MyCustomHeader': 'My custom value' } + }); + + + allResourcesBeingLoaded.push(fetchResources); + fetchResources.then((r) => { + i++; + var total = allResourcesBeingLoaded.length; + if (typeof onResourceLoaded === "function") { + onResourceLoaded(i, total); + } + }); + return fetchResources; + } +}); \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/favicon.ico b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a4bff57dc00a25fdf5df329dd5c7afabc71050e9 GIT binary patch literal 128052 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⁣+Q;iZ0G%Yob=vP_d;R4T7yR7cVH%b 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πk} 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 Slo^}*0ov*YYIQ1zsQ&|$m~=Y; literal 0 HcmV?d00001 diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/index.html b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/index.html new file mode 100644 index 0000000..7a47d9b --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/index.html @@ -0,0 +1,21 @@ + + + + + FastControls.TestApp + + + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.css b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.css new file mode 100644 index 0000000..47a1f80 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.css @@ -0,0 +1,154 @@ +@keyframes loading-indicator-ball-anim { + 0% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } + + 25% { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + } + + 32% { + transform: translate(-50%, -50%) scale(0.5); + opacity: 0; + } + + 100% { + transform: translate(-50%, -50%) scale(0); + opacity: 0; + } +} + +.loading-indicator-wrapper { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: 100%; + height: 100%; +} + +.loading-indicator { + position: relative; + width: 80px; + height: 80px; + pointer-events: none; +} + +.loading-indicator-ball { + will-change: transform, opacity; + position: absolute; + width: 16%; + height: 16%; + border-radius: 50%; + background: #41afe6; + filter: blur(4px); + opacity: 0; + animation: loading-indicator-ball-anim 9s infinite; +} + + .loading-indicator-ball:nth-child(1) { + left: 85.3553390593%; + top: 85.3553390593%; + animation-delay: 0s; + } + + .loading-indicator-ball:nth-child(2) { + left: 100%; + top: 50%; + animation-delay: 0.2s; + } + + .loading-indicator-ball:nth-child(3) { + left: 85.3553390593%; + top: 14.6446609407%; + --rotation: calc(-45deg * 3); + animation-delay: 0.4s; + } + + .loading-indicator-ball:nth-child(4) { + left: 50%; + top: 0%; + animation-delay: 0.6s; + } + + .loading-indicator-ball:nth-child(5) { + left: 14.6446609407%; + top: 14.6446609407%; + animation-delay: 0.8s; + } + + .loading-indicator-ball:nth-child(6) { + left: 0%; + top: 50%; + animation-delay: 1.0s; + } + + .loading-indicator-ball:nth-child(7) { + left: 14.6446609407%; + top: 85.3553390593%; + animation-delay: 1.2s; + } + + .loading-indicator-ball:nth-child(8) { + left: 50%; + top: 100%; + animation-delay: 1.4s; + } + + .loading-indicator-ball:nth-child(9) { + left: 50%; + top: 100%; + animation-delay: 4.5s; + } + + .loading-indicator-ball:nth-child(10) { + left: 14.6446609407%; + top: 85.3553390593%; + animation-delay: 4.7s; + } + + .loading-indicator-ball:nth-child(11) { + left: 0%; + top: 50%; + animation-delay: 4.9s; + } + + .loading-indicator-ball:nth-child(12) { + left: 14.6446609407%; + top: 14.6446609407%; + animation-delay: 5.1s; + } + + .loading-indicator-ball:nth-child(13) { + left: 50%; + top: 0%; + animation-delay: 5.3s; + } + + .loading-indicator-ball:nth-child(14) { + left: 85.3553390593%; + top: 14.6446609407%; + animation-delay: 5.5s; + } + + .loading-indicator-ball:nth-child(15) { + left: 100%; + top: 50%; + animation-delay: 5.7s; + } + + .loading-indicator-ball:nth-child(16) { + left: 85.3553390593%; + top: 85.3553390593%; + animation-delay: 5.9s; + } + +.loading-indicator-percentage-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.js b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.js new file mode 100644 index 0000000..4471b52 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/loading-indicator.js @@ -0,0 +1,28 @@ +function onResourceLoaded(resourceIndex, totalResourceCount) { + let percentage = document.getElementById("loading-indicator-percentage"); + if (percentage) { + percentage.innerHTML = Math.round((resourceIndex / totalResourceCount) * 100) + "%"; + } +} + +let loadingIndicatorWrapper = document.createElement("div"); +loadingIndicatorWrapper.classList.add("loading-indicator-wrapper"); +document.getElementById("app").appendChild(loadingIndicatorWrapper); + +let loadingIndicator = document.createElement("div"); +loadingIndicator.classList.add("loading-indicator"); +loadingIndicatorWrapper.appendChild(loadingIndicator); + +for (let i = 0; i < 16; i++) { + let loadingIndicatorBall = document.createElement("div"); + loadingIndicatorBall.classList.add("loading-indicator-ball"); + loadingIndicator.appendChild(loadingIndicatorBall); +} + +let loadingIndicatorPercentageContainer = document.createElement("div"); +loadingIndicatorPercentageContainer.classList.add("loading-indicator-percentage-container"); +loadingIndicator.appendChild(loadingIndicatorPercentageContainer); + +let loadingIndicatorPercentage = document.createElement("div"); +loadingIndicatorPercentage.id = "loading-indicator-percentage"; +loadingIndicatorPercentageContainer.appendChild(loadingIndicatorPercentage); \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj b/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj new file mode 100644 index 0000000..1d1be8f --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj @@ -0,0 +1,16 @@ + + + + WinExe + net472 + + + + + + + + + + + \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Simulator/Startup.cs b/src/FastControls.TestApp/FastControls.TestApp.Simulator/Startup.cs new file mode 100644 index 0000000..cb0d415 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Simulator/Startup.cs @@ -0,0 +1,14 @@ +using OpenSilver.Simulator; +using System; + +namespace FastControls.TestApp.Simulator +{ + internal static class Startup + { + [STAThread] + static int Main(string[] args) + { + return SimulatorLauncher.Start(typeof(App)); + } + } +} diff --git a/src/FastControls.TestApp/FastControls.TestApp.sln b/src/FastControls.TestApp/FastControls.TestApp.sln new file mode 100644 index 0000000..20f06ec --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33103.184 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastControls.TestApp.Browser", "FastControls.TestApp.Browser\FastControls.TestApp.Browser.csproj", "{2FF0F73A-CA05-4CD3-86DA-EAD7F9879B92}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastControls.TestApp", "FastControls.TestApp\FastControls.TestApp.csproj", "{90FA3477-01EA-4A80-9F56-8B159893B173}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastControls.TestApp.Simulator", "FastControls.TestApp.Simulator\FastControls.TestApp.Simulator.csproj", "{C75B8373-6086-4379-B91F-44280050B051}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2FF0F73A-CA05-4CD3-86DA-EAD7F9879B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FF0F73A-CA05-4CD3-86DA-EAD7F9879B92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FF0F73A-CA05-4CD3-86DA-EAD7F9879B92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FF0F73A-CA05-4CD3-86DA-EAD7F9879B92}.Release|Any CPU.Build.0 = Release|Any CPU + {90FA3477-01EA-4A80-9F56-8B159893B173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90FA3477-01EA-4A80-9F56-8B159893B173}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90FA3477-01EA-4A80-9F56-8B159893B173}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90FA3477-01EA-4A80-9F56-8B159893B173}.Release|Any CPU.Build.0 = Release|Any CPU + {C75B8373-6086-4379-B91F-44280050B051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C75B8373-6086-4379-B91F-44280050B051}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C75B8373-6086-4379-B91F-44280050B051}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C75B8373-6086-4379-B91F-44280050B051}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {246E19BE-C1F7-4F49-9DF6-480E3ADA07D0} + EndGlobalSection +EndGlobal diff --git a/src/FastControls.TestApp/FastControls.TestApp/App.xaml b/src/FastControls.TestApp/FastControls.TestApp/App.xaml new file mode 100644 index 0000000..35cc07c --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/App.xaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs new file mode 100644 index 0000000..bdc284e --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; + +namespace FastControls.TestApp +{ + public sealed partial class App : Application + { + public App() + { + this.InitializeComponent(); + + // Enter construction logic here... + + var mainPage = new MainPage(); + Window.Current.Content = mainPage; + } + } +} diff --git a/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj new file mode 100644 index 0000000..fbf903f --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + false + true + + + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + + + + + + + + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml b/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml new file mode 100644 index 0000000..c79191e --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml.cs new file mode 100644 index 0000000..59c41da --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/MainPage.xaml.cs @@ -0,0 +1,60 @@ +using FastControls.TestApp.Registry; +using System; +using System.Windows.Controls; + +namespace FastControls.TestApp +{ + public partial class MainPage : Page + { + public MainPage() + { + this.InitializeComponent(); + + // Enter construction logic here... + foreach (var i in TestRegistry.Tests) + { + CreateTreeItem(i, MenuContainer.Items); + } + } + + private void CreateTreeItem(TreeItem treeItem, ItemCollection parent) + { + if (treeItem.IsLeaf) + { + TreeViewItem treeViewItem = new TreeViewItem + { + Header = treeItem.Name + }; + treeViewItem.Selected += (sender, e) => { + NavigateToPage(treeItem.FileName); + }; + + parent.Add(treeViewItem); + } + else + { + TreeViewItem treeViewItem = new TreeViewItem + { + Header = treeItem.Name + }; + + parent.Add(treeViewItem); + + foreach (var child in treeItem.Children) + { + CreateTreeItem(child, treeViewItem.Items); + } + } + } + + private void NavigateToPage(string pageName) + { + // Navigate to the target page: + Uri uri = new Uri(string.Format("/{0}Page", pageName), UriKind.Relative); + ContentContainer.Source = uri; + + // Scroll to top: + ScrollViewer1.ScrollToVerticalOffset(0d); + } + } +} diff --git a/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml b/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml new file mode 100644 index 0000000..894e296 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml @@ -0,0 +1,10 @@ + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml.cs new file mode 100644 index 0000000..402c322 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/Pages/FastCheckBoxPage.xaml.cs @@ -0,0 +1,18 @@ +using System.Windows; +using System.Windows.Controls; + +namespace FastControls.TestApp.Pages +{ + public partial class FastCheckBoxPage : UserControl + { + public FastCheckBoxPage() + { + InitializeComponent(); + } + + public void OnClick(object sender, RoutedEventArgs e) + { + MessageBox.Show("Checkbox has been clicked"); + } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp/Properties/launchSettings.json b/src/FastControls.TestApp/FastControls.TestApp/Properties/launchSettings.json new file mode 100644 index 0000000..dd9f6a2 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "FastControls.TestApp": { + "commandName": "Executable", + "executablePath": "cmd", + "commandLineArgs": "/c start \"\" \"http://www.opensilver.net/permalinks/wrong_startup_project.aspx\"" + } + } +} \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp/Registry/TestRegistry.cs b/src/FastControls.TestApp/FastControls.TestApp/Registry/TestRegistry.cs new file mode 100644 index 0000000..2ec57fc --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/Registry/TestRegistry.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FastControls.TestApp.Registry +{ + internal class TestRegistry + { + public static readonly IReadOnlyList Tests; + + static TestRegistry() + { + Tests = new [] + { + new TreeItem("FastCheckBox", "FastCheckBox") + }; + } + } +} diff --git a/src/FastControls.TestApp/FastControls.TestApp/Registry/TreeItem.cs b/src/FastControls.TestApp/FastControls.TestApp/Registry/TreeItem.cs new file mode 100644 index 0000000..e2e861e --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/Registry/TreeItem.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FastControls.TestApp.Registry +{ + internal class TreeItem + { + public string Name { get; } + + public string FileName { get; } + + public IReadOnlyList Children { get; } + + public bool IsLeaf + { + get => Children == null || Children.Count == 0; + } + + public TreeItem(string name, string fileName) + { + Name = name; + FileName = fileName; + } + + public TreeItem(string name, IEnumerable children) + { + Name = name; + Children = children.ToList().AsReadOnly(); + } + } +} diff --git a/src/FastControls/FastCheckBox.cs b/src/FastControls/FastCheckBox.cs new file mode 100644 index 0000000..9f95347 --- /dev/null +++ b/src/FastControls/FastCheckBox.cs @@ -0,0 +1,257 @@ +/*=================================================================================== +* +* Copyright (c) Userware (OpenSilver.net) +* +* This file is part of the OpenSilver.ControlsKit (https://opensilver.net), which +* is licensed under the MIT license (https://opensource.org/licenses/MIT). +* +* As stated in the MIT license, "the above copyright notice and this permission +* notice shall be included in all copies or substantial portions of the Software." +* +*====================================================================================*/ + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using CSHTML5.Internal; + +namespace OpenSilver.ControlsKit +{ + public class FastCheckBox : Control + { + public static readonly DependencyProperty IsCheckedProperty = + DependencyProperty.Register( + "IsChecked", + typeof(bool?), + typeof(FastCheckBox), + new PropertyMetadata(false, OnIsCheckedChanged) + { + CallPropertyChangedWhenLoadedIntoVisualTree = WhenToCallPropertyChangedEnum.IfPropertyIsSet + }); + + public static readonly DependencyProperty IsThreeStateProperty = + DependencyProperty.Register( + "IsThreeState", + typeof(bool), + typeof(FastCheckBox), + new PropertyMetadata(false)); + + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register("Text", + typeof(string), + typeof(FastCheckBox), + new PropertyMetadata(string.Empty, OnTextChanged) + { + CallPropertyChangedWhenLoadedIntoVisualTree = WhenToCallPropertyChangedEnum.IfPropertyIsSet + }); + + private object _checkboxHtmlElementRef; + + private JavascriptCallback _jsCallbackOnClick; + private object _labelHtmlElementRef; + private object _spanHtmlElementRef; + + public FastCheckBox() + { + Unloaded += (s, e) => DisposeJsCallbacks(); + } + + internal override bool EnablePointerEventsCore + { + get { return true; } + } + + public bool? IsChecked + { + get => (bool?)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } + + public bool IsThreeState + { + get => (bool)GetValue(IsThreeStateProperty); + set => SetValue(IsThreeStateProperty, value); + } + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public event RoutedEventHandler Checked; + public event RoutedEventHandler Indeterminate; + public event RoutedEventHandler Unchecked; + public event RoutedEventHandler Click; + + private void DisposeJsCallbacks() + { + _jsCallbackOnClick?.Dispose(); + _jsCallbackOnClick = null; + } + + public override object CreateDomElement(object parentRef, out object domElementWhereToPlaceChildren) + { + INTERNAL_HtmlDomManager.CreateDomElementAppendItAndGetStyle("label", parentRef, this, + out _labelHtmlElementRef); + + _jsCallbackOnClick = JavascriptCallback.Create((Action)(() => + { + OnToggle(); + + // Trigger click notifications + OnClick(new RoutedEventArgs + { + OriginalSource = this + }); + })); + // Subscribe to the javascript click event through a simple listener + Interop.ExecuteJavaScript("$0.onclick = function(e) { e.preventDefault(); $1(); }", + _labelHtmlElementRef, _jsCallbackOnClick); + + INTERNAL_HtmlDomManager.CreateDomElementAppendItAndGetStyle( + "input", + _labelHtmlElementRef, this, out _checkboxHtmlElementRef); + INTERNAL_HtmlDomManager.SetDomElementAttribute(_checkboxHtmlElementRef, "type", "checkbox"); + + INTERNAL_HtmlDomManager.CreateDomElementAppendItAndGetStyle("span", _labelHtmlElementRef, this, + out _spanHtmlElementRef); + + domElementWhereToPlaceChildren = _labelHtmlElementRef; + return _labelHtmlElementRef; + } + + protected virtual void UpdateCheckInterop() + { + if (IsChecked == true) + { + Interop.ExecuteJavaScript("$0.checked = true; $0.indeterminate = undefined", _checkboxHtmlElementRef); + } + else if (IsChecked.HasValue) + { + Interop.ExecuteJavaScript("$0.checked = false; $0.indeterminate = undefined", _checkboxHtmlElementRef); + } + else + { + Interop.ExecuteJavaScript("$0.checked = undefined; $0.indeterminate = true", _checkboxHtmlElementRef); + } + } + + protected internal virtual void OnToggle() + { + // If IsChecked == true && IsThreeState == true ---> IsChecked = null + // If IsChecked == true && IsThreeState == false ---> IsChecked = false + // If IsChecked == false ---> IsChecked = true + // If IsChecked == null ---> IsChecked = false + bool? isChecked; + if (IsChecked == true) + { + isChecked = IsThreeState ? null : (bool?)false; + } + else // false or null + { + isChecked = IsChecked.HasValue; // HasValue returns true if IsChecked==false + } + + SetCurrentValue(IsCheckedProperty, isChecked); + } + + protected virtual void OnClick(RoutedEventArgs e) + { + Click?.Invoke(this, e); + } + + protected virtual void OnChecked(RoutedEventArgs e) + { + Checked?.Invoke(this, e); + } + + protected virtual void OnIndeterminate(RoutedEventArgs e) + { + Indeterminate?.Invoke(this, e); + } + + protected virtual void OnUnchecked(RoutedEventArgs e) + { + Unchecked?.Invoke(this, e); + } + + private static void OnIsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + FastCheckBox checkbox = (FastCheckBox)d; + if (checkbox == null || !checkbox.IsLoaded) + { + return; + } + + bool? newValue = (bool?)e.NewValue; + + checkbox.UpdateCheckInterop(); + + if (newValue == true) + { + checkbox.OnChecked(new RoutedEventArgs + { + OriginalSource = checkbox + }); + } + else if (newValue == false) + { + checkbox.OnUnchecked(new RoutedEventArgs + { + OriginalSource = checkbox + }); + } + else + { + checkbox.OnIndeterminate(new RoutedEventArgs + { + OriginalSource = checkbox + }); + } + } + + private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + FastCheckBox element = (FastCheckBox)d; + if (element.IsLoaded && e.NewValue != null) + { + Interop.ExecuteJavaScript( + "if($0.firstChild) { $0.removeChild($0.firstChild) }; $0.appendChild(document.createTextNode($1));", + element._spanHtmlElementRef, e.NewValue); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + if (!(_checkboxHtmlElementRef is INTERNAL_HtmlDomElementReference) || + !(_spanHtmlElementRef is INTERNAL_HtmlDomElementReference)) + { + return new Size(); + } + + // Get actual width with margin + string sizeString = Interop.ExecuteJavaScript(@" +((parseInt(window.getComputedStyle($0).getPropertyValue('margin-left')) | 0) + (parseInt(window.getComputedStyle($0).getPropertyValue('margin-right')) | 0) + $0['offsetWidth'] + +(parseInt(window.getComputedStyle($1).getPropertyValue('margin-left')) | 0) + (parseInt(window.getComputedStyle($1).getPropertyValue('margin-right')) | 0) + $1['offsetWidth']).toFixed(3) + '|' + +Math.max((parseInt(window.getComputedStyle($0).getPropertyValue('margin-top')) | 0) + (parseInt(window.getComputedStyle($0).getPropertyValue('margin-bottom')) | 0) + $0['offsetHeight'], +(parseInt(window.getComputedStyle($1).getPropertyValue('margin-top')) | 0) + (parseInt(window.getComputedStyle($1).getPropertyValue('margin-bottom')) | 0) + $1['offsetHeight']).toFixed(3);", + _checkboxHtmlElementRef, _spanHtmlElementRef).ToString(); + + int sepIndex = sizeString.IndexOf('|'); + if (sepIndex <= -1) + { + return new Size(); + } + + string actualWidthAsString = sizeString.Substring(0, sepIndex); + string actualHeightAsString = sizeString.Substring(sepIndex + 1); + double actualWidth = double.Parse(actualWidthAsString, + CultureInfo.InvariantCulture); + double actualHeight = double.Parse(actualHeightAsString, + CultureInfo.InvariantCulture); + return new Size(actualWidth, actualHeight); + } + } +} \ No newline at end of file diff --git a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj index f2c0c84..61955df 100644 --- a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj +++ b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj @@ -5,4 +5,8 @@ OpenSilver.ControlsKit + + + + diff --git a/src/Nuget.Config b/src/Nuget.Config new file mode 100644 index 0000000..3c0f490 --- /dev/null +++ b/src/Nuget.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/OpenSilver.ControlsKit.sln b/src/OpenSilver.ControlsKit.sln index 72ddc7d..0ea45fc 100644 --- a/src/OpenSilver.ControlsKit.sln +++ b/src/OpenSilver.ControlsKit.sln @@ -3,7 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33103.184 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSilver.ControlsKit.FastControls", "FastControls\OpenSilver.ControlsKit.FastControls.csproj", "{A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSilver.ControlsKit.FastControls", "FastControls\OpenSilver.ControlsKit.FastControls.csproj", "{A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenSilver.ControlsKit.FastControls.TestApp", "OpenSilver.ControlsKit.FastControls.TestApp", "{757990DC-A1AE-4E39-A086-A6C4FA59B250}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastControls.TestApp", "FastControls.TestApp\FastControls.TestApp\FastControls.TestApp.csproj", "{6EF229A1-0A2A-4452-8E57-88156D2650EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastControls.TestApp.Browser", "FastControls.TestApp\FastControls.TestApp.Browser\FastControls.TestApp.Browser.csproj", "{569ABA81-F8A2-4A3E-9155-555A3EAB0F62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastControls.TestApp.Simulator", "FastControls.TestApp\FastControls.TestApp.Simulator\FastControls.TestApp.Simulator.csproj", "{B28B4B1C-A738-4D9F-96A7-6DCC231A9928}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,10 +23,27 @@ Global {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8FAB0AB-A9EC-4FC6-B033-74ABBFE9DD9D}.Release|Any CPU.Build.0 = Release|Any CPU + {6EF229A1-0A2A-4452-8E57-88156D2650EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EF229A1-0A2A-4452-8E57-88156D2650EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EF229A1-0A2A-4452-8E57-88156D2650EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EF229A1-0A2A-4452-8E57-88156D2650EC}.Release|Any CPU.Build.0 = Release|Any CPU + {569ABA81-F8A2-4A3E-9155-555A3EAB0F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {569ABA81-F8A2-4A3E-9155-555A3EAB0F62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {569ABA81-F8A2-4A3E-9155-555A3EAB0F62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {569ABA81-F8A2-4A3E-9155-555A3EAB0F62}.Release|Any CPU.Build.0 = Release|Any CPU + {B28B4B1C-A738-4D9F-96A7-6DCC231A9928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B28B4B1C-A738-4D9F-96A7-6DCC231A9928}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B28B4B1C-A738-4D9F-96A7-6DCC231A9928}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B28B4B1C-A738-4D9F-96A7-6DCC231A9928}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6EF229A1-0A2A-4452-8E57-88156D2650EC} = {757990DC-A1AE-4E39-A086-A6C4FA59B250} + {569ABA81-F8A2-4A3E-9155-555A3EAB0F62} = {757990DC-A1AE-4E39-A086-A6C4FA59B250} + {B28B4B1C-A738-4D9F-96A7-6DCC231A9928} = {757990DC-A1AE-4E39-A086-A6C4FA59B250} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3095C2CC-DAE8-4223-BF01-AFDDF80767AD} EndGlobalSection From 01d49303d77dac0b4a72c8ad73bdd3c67af563db Mon Sep 17 00:00:00 2001 From: Iakov Lilo Date: Thu, 10 Nov 2022 18:40:46 +1100 Subject: [PATCH 3/8] chore: add ci-cd --- .github/workflows/github-actions.yml | 56 +++++++++++++++++++ src/Directory.Build.targets | 7 +++ .../FastControls.TestApp.Browser.csproj | 2 +- .../FastControls.TestApp.Simulator.csproj | 2 +- .../FastControls.TestApp.csproj | 2 +- ...OpenSilver.ControlsKit.FastControls.csproj | 12 +++- .../build/build-nuget-package.bat | 9 +++ 7 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/github-actions.yml create mode 100644 src/Directory.Build.targets create mode 100644 src/FastControls/build/build-nuget-package.bat diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..f8a658b --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,56 @@ +name: OpenSilver.ControlsKit Build +env: + next-release-version: '1.2.0' + opensilver-package-source: 'https://www.myget.org/F/opensilver/api/v3/index.json' + suffix: 'preview' +on: + push: + branches: + - master + workflow_dispatch: + inputs: + opensilver-version: + description: 'OpenSilver package version' + default: 'latest' + required: true +jobs: + OpenSilver-ControlsKit-Build: + #We should not run these steps on the forks by default. + if: github.repository_owner == 'OpenSilver' + runs-on: windows-latest + steps: + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.1.3 + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.x + - name: Install DotNet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + - name: Clone repo + uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + - name: Fill vars + id: vars + run: | + $version = "${{ github.event.inputs.opensilver-version }}" + $source = "${{ env.opensilver-package-source }}" + if ($version -eq "latest" -or $version -eq "") { + $version = nuget list -Source $source -Prerelease | ? { $_ -match "^OpenSilver\s+(.*)" } | ForEach { $_.split(" ")[1] } + } + echo "Version: $version" + echo "opensilver-version=$version" >> $env:GITHUB_OUTPUT + echo "package-version=${{ env.next-release-version }}-${{ env.suffix }}-$(date +'%Y-%m-%d-%H%M%S')-${{ env.GITHUB_SHA_SHORT }}" >> $env:GITHUB_OUTPUT + - name: Replace OpenSilver PackageVersion + run: | + sed -i 's/[^<]*${{ steps.vars.outputs.opensilver-version }} + + 1.2.0-preview-2022-11-10-172629-0a505546 + true + latest + + \ No newline at end of file diff --git a/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj b/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj index 7f2be62..793c434 100644 --- a/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/FastControls.TestApp.Browser.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj b/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj index 1d1be8f..2d18631 100644 --- a/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj +++ b/src/FastControls.TestApp/FastControls.TestApp.Simulator/FastControls.TestApp.Simulator.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj index fbf903f..c7818d9 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj +++ b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj index 61955df..e4f3686 100644 --- a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj +++ b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj @@ -3,10 +3,20 @@ netstandard2.0 OpenSilver.ControlsKit + Userware + https://www.opensilver.net + The ControlsKit is a collection of additional libraries, controls, and helpers that developers can use directly in their OpenSilver projects, to complement the built-in controls. + Copyright (c) 2022, Userware. All rights reserved. + OpenSilver FastControls + https://github.com/OpenSilver/OpenSilver.ControlsKit + + https://github.com/OpenSilver/OpenSilver.ControlsKit + git + OpenSilver Controls Xaml - + diff --git a/src/FastControls/build/build-nuget-package.bat b/src/FastControls/build/build-nuget-package.bat new file mode 100644 index 0000000..09da8f7 --- /dev/null +++ b/src/FastControls/build/build-nuget-package.bat @@ -0,0 +1,9 @@ +@echo off +dotnet restore %~dp0\..\OpenSilver.ControlsKit.FastControls.csproj +set PackageVersion=%1 + +if /i "%PackageVersion%" EQU "" ( + set /p PackageVersion="%ESC%Package version:%ESC% " +) + +msbuild %~dp0\..\OpenSilver.ControlsKit.FastControls.csproj -t:pack -p:PackageVersion=%PackageVersion% -p:Configuration=Release -p:DebugSymbols=false -p:DebugType=None -p:Optimize=true \ No newline at end of file From 4cdeffd4e8f5db3ac29c5fa5e643d779c1eb5a42 Mon Sep 17 00:00:00 2001 From: Iakov Lilo Date: Mon, 14 Nov 2022 09:47:44 +1100 Subject: [PATCH 4/8] chore: add a step to upload the package to myget --- .github/workflows/github-actions.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index f8a658b..2fb2b21 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -54,3 +54,6 @@ jobs: with: name: FastControls path: src\FastControls\bin\Release\*.nupkg + - name: Upload packages to MyGet + run: | + dotnet nuget push "src\FastControls\bin\Release\*.nupkg" -k ${{ secrets.MYGET_TOKEN }} -s ${{ secrets.MYGET_PUBLIC_FEED }} \ No newline at end of file From a81eefa7263d37923cf9177f0630da2e2e01139e Mon Sep 17 00:00:00 2001 From: Iakov Lilo Date: Tue, 15 Nov 2022 09:49:39 +1100 Subject: [PATCH 5/8] chore: fix description --- src/FastControls/FastCheckBox.cs | 3 +-- src/FastControls/OpenSilver.ControlsKit.FastControls.csproj | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FastControls/FastCheckBox.cs b/src/FastControls/FastCheckBox.cs index 9f95347..783a4f0 100644 --- a/src/FastControls/FastCheckBox.cs +++ b/src/FastControls/FastCheckBox.cs @@ -13,12 +13,11 @@ using System; using System.Globalization; using System.Windows; -using System.Windows.Controls; using CSHTML5.Internal; namespace OpenSilver.ControlsKit { - public class FastCheckBox : Control + public class FastCheckBox : FrameworkElement { public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register( diff --git a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj index e4f3686..063cc98 100644 --- a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj +++ b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj @@ -5,7 +5,7 @@ OpenSilver.ControlsKit Userware https://www.opensilver.net - The ControlsKit is a collection of additional libraries, controls, and helpers that developers can use directly in their OpenSilver projects, to complement the built-in controls. + The ControlsKit.FastControls is a collection of fast controls that developers can use directly in their OpenSilver projects, to complement the built-in controls. Copyright (c) 2022, Userware. All rights reserved. OpenSilver FastControls https://github.com/OpenSilver/OpenSilver.ControlsKit From d751b911cd97079bfaccc07ef3e1b39efbc9e937 Mon Sep 17 00:00:00 2001 From: John Torjo Date: Tue, 24 Jan 2023 15:04:34 +0200 Subject: [PATCH 6/8] fastgrid - first version --- .../FastControls.TestApp/App.xaml.cs | 4 +- .../FastControls.TestApp.csproj | 5 + .../FastControls.TestApp/MockViewModel.cs | 156 ++ .../TestFastGridView.xaml | 111 ++ .../TestFastGridView.xaml.cs | 244 +++ src/FastControls/FastGrid/BrushCache.cs | 41 + .../FastGrid/FastGridContentTemplate.cs | 193 +++ src/FastControls/FastGrid/FastGridUtil.cs | 120 ++ src/FastControls/FastGrid/FastGridView.xaml | 23 + .../FastGrid/FastGridView.xaml.cs | 1368 +++++++++++++++++ src/FastControls/FastGrid/FastGridViewCell.cs | 75 + .../FastGrid/FastGridViewColumn.cs | 190 +++ .../FastGrid/FastGridViewColumnCollection.cs | 21 + .../FastGridViewColumnCollectionInternal.cs | 39 + .../FastGrid/FastGridViewFilter.cs | 30 + .../FastGrid/FastGridViewFilterItem.cs | 235 +++ src/FastControls/FastGrid/FastGridViewRow.cs | 264 ++++ .../FastGrid/FastGridViewRowContent.cs | 41 + src/FastControls/FastGrid/FastGridViewSort.cs | 177 +++ .../FastGrid/FastGridViewSortDescriptors.cs | 46 + ...OpenSilver.ControlsKit.FastControls.csproj | 6 + 21 files changed, 3387 insertions(+), 2 deletions(-) create mode 100644 src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs create mode 100644 src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml create mode 100644 src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs create mode 100644 src/FastControls/FastGrid/BrushCache.cs create mode 100644 src/FastControls/FastGrid/FastGridContentTemplate.cs create mode 100644 src/FastControls/FastGrid/FastGridUtil.cs create mode 100644 src/FastControls/FastGrid/FastGridView.xaml create mode 100644 src/FastControls/FastGrid/FastGridView.xaml.cs create mode 100644 src/FastControls/FastGrid/FastGridViewCell.cs create mode 100644 src/FastControls/FastGrid/FastGridViewColumn.cs create mode 100644 src/FastControls/FastGrid/FastGridViewColumnCollection.cs create mode 100644 src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs create mode 100644 src/FastControls/FastGrid/FastGridViewFilter.cs create mode 100644 src/FastControls/FastGrid/FastGridViewFilterItem.cs create mode 100644 src/FastControls/FastGrid/FastGridViewRow.cs create mode 100644 src/FastControls/FastGrid/FastGridViewRowContent.cs create mode 100644 src/FastControls/FastGrid/FastGridViewSort.cs create mode 100644 src/FastControls/FastGrid/FastGridViewSortDescriptors.cs diff --git a/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs index bdc284e..3a667d1 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs +++ b/src/FastControls.TestApp/FastControls.TestApp/App.xaml.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows; using System.Windows.Controls; +using FastGrid.FastGrid; namespace FastControls.TestApp { @@ -15,8 +16,7 @@ public App() // Enter construction logic here... - var mainPage = new MainPage(); - Window.Current.Content = mainPage; + Window.Current.Content = new TestFastGridView(); } } } diff --git a/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj index c7818d9..9f8d7db 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj +++ b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj @@ -20,14 +20,19 @@ MSBuild:Compile + + MSBuild:Compile + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs new file mode 100644 index 0000000..f9e123a --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Windows.Media; +using FastGrid.FastGrid; + +namespace FastControls.TestApp +{ + + public class Pullout : INotifyPropertyChanged{ + private int operatorRecordId; + private string operatorReportLabel = ""; + private string password = ""; + private string username = ""; + private string department; + private string city; + private int vehicleId; + private int pulloutId; + + public int PulloutId { + get => pulloutId; + set { + if (value == pulloutId) return; + pulloutId = value; + OnPropertyChanged(); + } + } + + // must match the Operator.ReportLabel + public string OperatorReportLabel { + get => operatorReportLabel; + set { + if (value == operatorReportLabel) return; + operatorReportLabel = value; + OnPropertyChanged(); + } + } + + public int OperatorRecordId { + get => operatorRecordId; + set { + if (value == operatorRecordId) return; + operatorRecordId = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(BgColor)); + OnPropertyChanged(nameof(IsActive)); + } + } + + public string Username + { + get => username; + set + { + if (value == username) return; + username = value; + OnPropertyChanged(); + } + } + + public string Password + { + get => password; + set + { + if (value == password) return; + password = value; + OnPropertyChanged(); + } + } + + public string Department + { + get => department; + set + { + if (value == department) return; + department = value; + OnPropertyChanged(); + } + } + + public string City + { + get => city; + set + { + if (value == city) return; + city = value; + OnPropertyChanged(); + } + } + + public Brush BgColor { + get { + switch (OperatorRecordId % 4) { + case 0: return BrushCache.Inst.GetByColor(Colors.CornflowerBlue); + case 1: return BrushCache.Inst.GetByColor(Colors.DodgerBlue); + case 2: return BrushCache.Inst.GetByColor(Colors.LightSeaGreen); + case 3: return BrushCache.Inst.GetByColor(Colors.DarkSlateBlue); + default: + Debug.Assert(false); + return BrushCache.Inst.GetByColor(Colors.DarkSlateBlue); + } + } + } + + public int VehicleId { + get => vehicleId; + set { + if (value == vehicleId) return; + vehicleId = value; + OnPropertyChanged(); + } + } + + public bool IsActive => (OperatorRecordId % 10) == 0; + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + + public class MockViewModel + { + + public MockViewModel() { + } + + public IEnumerable GetPulloutsByCount(int count, int offset = 0) { + for (int i = offset; i < count + offset; ++i) + yield return new Pullout { + OperatorReportLabel = $"Operator {i}" , + OperatorRecordId = i, + VehicleId = i , + Username = $"User {i}", + Password = $"Pass {i}", + Department = $"Dep {i}", + City = $"City {i}", + }; + + } + + } + + + + + + + +} diff --git a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml new file mode 100644 index 0000000..39843f3 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs new file mode 100644 index 0000000..0634684 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using FastControls.TestApp; +using FastGrid.FastGrid; + +namespace FastGrid.FastGrid +{ + public partial class TestFastGridView : Page + { + private ObservableCollection _pullouts; + public TestFastGridView() + { + this.InitializeComponent(); + } + + private async Task TestSimulateScroll() + { + for (int i = 0; i < 200; ++i) + { + ctrl.VerticalScrollToRowIndex(i + 1); + await Task.Delay(50); + } + for (int i = 200; i >= 0; --i) + { + ctrl.VerticalScrollToRowIndex(i + 1); + await Task.Delay(50); + } + } + + private int RefIndex(Pullout pullout) + { + int idx = 0; + _pullouts.FirstOrDefault(i => + { + if (ReferenceEquals(i, pullout)) + return true; + ++idx; + return false; + }); + return idx; + } + private async Task TestSimulateInsertionDeletions() + { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + await Task.Delay(2000); + + var rowIdx = 100; + var newInsertions = 300; + var delay = 50; + var top = _pullouts[rowIdx]; + ctrl.ScrollToRow(top); + var sel = _pullouts[rowIdx + 1]; + ctrl.SelectedItem = sel; + await Task.Delay(1000); + + // compute rowidx + topidx when things are inserted/deleted + // so the idea is: topidx is recomputed on each redraw + + var newPullouts = new MockViewModel().GetPulloutsByCount(newInsertions, 1000).ToList(); + for (int i = 0; i < newInsertions; ++i) + { + // the idea - in this test, the selection is set via an item + var topIdx = RefIndex(top); + // this should not affect anything visually + _pullouts.Insert(topIdx - 10, newPullouts[i]); + // this should insert a row, visually + _pullouts.Insert(topIdx + 4, newPullouts[i]); + // this should delete a row, visually + _pullouts.RemoveAt(topIdx + 19); + await Task.Delay(delay); + Debug.WriteLine($"insert {i}"); + } + } + + private async Task TestConstantUpdates() + { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + await Task.Delay(2000); + + var rowIdx = 50; + var maxRows = 25; + ctrl.ScrollToRow(_pullouts[rowIdx]); + var colOffset = 0; + var operationIdx = 0; + for (int i = 0; i < 400; ++i) + { + ctrl.SelectedIndex = rowIdx + (i % maxRows); + for (int j = 0; j < 20; ++j) + { + var row = _pullouts[rowIdx + ((i + j) % maxRows)]; + switch (colOffset) + { + case 0: + row.OperatorReportLabel = DateTime.Now.Ticks.ToString(); + break; + case 1: + row.OperatorRecordId++; + break; + case 2: + row.Username = $"user {operationIdx}"; + break; + case 3: + row.Password = $"pass {operationIdx}"; + break; + case 4: + row.Department = $"dep {operationIdx}"; + break; + case 5: + row.City = $"city {operationIdx}"; + break; + } + + colOffset = (colOffset + 1) % 6; + ++operationIdx; + } + await Task.Delay(50); + } + } + + private async Task TestBoundBackground() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + ctrl.RowTemplate = FastGridContentTemplate.BindBackgroundRowTemplate("BgColor"); + await Task.Delay(2000); + + // increment the OperatorID of each one + for (int i = 0; i < 400; ++i) { + foreach (var pullout in _pullouts) + pullout.OperatorRecordId++; + await Task.Delay(250); + } + } + + private async Task TestAddAndRemoveSorted() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); + ctrl.ItemsSource = _pullouts; + + ctrl.SortDescriptors.Add(new FastGridSortDescriptor { Column = ctrl.Columns["OperatorReportLabel"], SortDirection = SortDirection.Descending}); + await Task.Delay(3000); + var extraPullouts = new MockViewModel().GetPulloutsByCount(50).ToList(); + for (int i = 0; i < extraPullouts.Count; ++i) { + _pullouts.Add(extraPullouts[i]); + await Task.Delay(250); + } + for (int i = 0; i < extraPullouts.Count; ++i) { + _pullouts.Remove(extraPullouts[i]); + await Task.Delay(250); + } + } + + private async Task TestResortingExistingItems() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); + ctrl.ItemsSource = _pullouts; + + ctrl.SortDescriptors.Add(new FastGridSortDescriptor { Column = ctrl.Columns["OperatorReportLabel"], SortDirection = SortDirection.Descending}); + await Task.Delay(3000); + + for (int i = 0; i < 100; ++i) { + _pullouts[0].OperatorReportLabel = $"Operator {i}"; + _pullouts[1].OperatorReportLabel = $"Operator {i+1}"; + await Task.Delay(250); + } + } + + private async Task TestRowBackgroundFunc() { + var reverseEven = false; + ctrl.RowBackgroundColorFunc = (o) => { + var pullout = o as Pullout; + var isEven = pullout.OperatorRecordId % 2 == 0; + if (reverseEven) + isEven = !isEven; + var color = isEven ? Colors.Gray : Colors.LightGray; + return BrushCache.Inst.GetByColor(color); + }; + + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + + for (int i = 0; i < 100; ++i) { + await Task.Delay(2000); + reverseEven = !reverseEven; + ctrl.Redraw(); + } + } + + private void SimpleTest() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + ctrl.AllowSortByMultipleColumns = false; + ctrl.Columns[1].Sort = true; + + ctrl.AllowMultipleSelection = true; + ctrl.SelectionChanged += (s, a) => { + var sel = Enumerable.OfType(ctrl.GetSelection()); + Console.WriteLine($"new selection {string.Join(",", sel.Select(p => p.Username))}"); + }; + } + + private async Task TestOffscreen() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + for (int i = 0; i < 50; ++i) { + await Task.Delay(4000); + Canvas.SetLeft(ctrl, (i % 2) == 0 ? -10000 : 0); + } + } + + private async void Page_Loaded(object sender, RoutedEventArgs e) { + SimpleTest(); + //await TestOffscreen(); + + //await TestRowBackgroundFunc(); + + //await TestAddAndRemoveSorted(); + //await TestResortingExistingItems(); + //await TestBoundBackground(); + //await TestSimulateScroll(); + //await TestSimulateInsertionDeletions(); + //await TestConstantUpdates(); + } + + private void ButtonViewXamlTree_Click(object sender, RoutedEventArgs e) { + Console.WriteLine("menu 1"); + } + + private void ButtonViewCompilationLog_Click(object sender, RoutedEventArgs e) + { + Console.WriteLine("menu 2"); + } + + private void ButtonExecuteJS_Click(object sender, RoutedEventArgs e) + { + Console.WriteLine("menu 3"); + } + } +} diff --git a/src/FastControls/FastGrid/BrushCache.cs b/src/FastControls/FastGrid/BrushCache.cs new file mode 100644 index 0000000..aa59a4a --- /dev/null +++ b/src/FastControls/FastGrid/BrushCache.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Media; + +namespace FastGrid.FastGrid +{ + public class BrushCache { + // how many max brushes we can cache + public int MaxBrushCache = 16 * 1024; + private BrushCache() { + } + + private Dictionary _solidBrushes = new Dictionary(); + + // FIXME not implemented yet + private Dictionary _linearBrushes = new Dictionary(); + + public static BrushCache Inst { get; } = new BrushCache(); + + private static uint ToInt(Color color) { + uint argb = color.A; + argb <<= 8; + argb += color.R; + argb <<= 8; + argb += color.G; + argb <<= 8; + argb += color.B; + return argb; + } + + public SolidColorBrush GetByColor(Color color) { + var key = ToInt(color); + if (_solidBrushes.TryGetValue(key, out var brush)) + return brush; + var newBrush = new SolidColorBrush(color); + _solidBrushes.Add(key, newBrush); + return newBrush; + } + } +} diff --git a/src/FastControls/FastGrid/FastGridContentTemplate.cs b/src/FastControls/FastGrid/FastGridContentTemplate.cs new file mode 100644 index 0000000..643fa70 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridContentTemplate.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Shapes; + +namespace FastGrid.FastGrid +{ + // data templates + // + // you cna also create your own, and use these as inspiration + public static class FastGridContentTemplate + { + public static DataTemplate BottomLineRowTemplate(Color color) { + var dt = FastGridUtil.CreateDataTemplate(() => { + var border = new Border { + BorderThickness = new Thickness(0,0,0,1), + BorderBrush = new SolidColorBrush(color), + }; + return border; + }); + return dt; + } + + public static DataTemplate BindBackgroundRowTemplate(string backgroundProperty) { + var dt = FastGridUtil.CreateDataTemplate(() => { + var border = new Border { + }; + border.SetBinding(Border.BackgroundProperty, new Binding(backgroundProperty)); + return border; + }); + return dt; + } + + public static DataTemplate DefaultRowTemplate() { + var dt = FastGridUtil.CreateDataTemplate(() => new Canvas()); + return dt; + } + + private static Geometry SortPath() { + var pf = new PathFigure { + StartPoint = new Point(3,0), + Segments = new PathSegmentCollection(new PathSegment[] { + new LineSegment { Point = new Point(6,4)}, + new LineSegment { Point = new Point(0,4)}, + }) + }; + var g = new PathGeometry(new PathFigure[] { pf }); + return g; + } + public static DataTemplate DefaultHeaderTemplate() { + var dt = FastGridUtil.CreateDataTemplate(() => { + const double MIN_WIDTH = 20; + /* + + + + */ + var grid = new Grid { + Background = new SolidColorBrush(Colors.Transparent) + }; + var tb = new TextBlock { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(10, 0, 10, 0), + FontSize = 14, + }; + grid.SetBinding(Grid.WidthProperty, new Binding("Width")); + grid.SetBinding(Grid.MinWidthProperty, new Binding("MinWidth")); + grid.SetBinding(Grid.MaxWidthProperty, new Binding("MaxWidth")); + grid.SetBinding(Grid.VisibilityProperty, new Binding("IsVisible") { Converter = new BooleanToVisibilityConverter() }); + + tb.SetBinding(TextBlock.TextProperty, new Binding("HeaderText")); + + var path = new Path { + Data = SortPath(), + Fill = new SolidColorBrush(Colors.Gray), + RenderTransformOrigin = new Point(0.5, 0.5), + Opacity = 0, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(10, 0, 10, 0), + Stretch = Stretch.Fill, + Width = 6, Height = 4, + RenderTransform = new RotateTransform { Angle = 180 }, + }; + + var canvas = new Canvas { + VerticalAlignment = VerticalAlignment.Stretch, + }; + var rect = new Rectangle { + Fill = new SolidColorBrush(Colors.Gray), + Width = 1, + }; + const int TRANSPARENT_WIDTH = 10; + var transparentRect = new Rectangle { + Fill = new SolidColorBrush(Colors.Transparent), + Width = TRANSPARENT_WIDTH, + Cursor = Cursors.SizeWE, + }; + canvas.Children.Add(rect); + canvas.Children.Add(transparentRect); + grid.Children.Add(tb); + grid.Children.Add(path); + grid.Children.Add(canvas); + grid.DataContextChanged += (s, a) => { + var column = (s as FrameworkElement).DataContext as FastGridViewColumn; + column.PropertyChanged += (ss,aa) => Column_PropertyChanged(grid); + Column_PropertyChanged(grid); + }; + grid.MouseLeftButtonUp += (s, a) => { + if ((s as FrameworkElement).DataContext is FastGridViewColumn column) { + if (column.DataBindingPropertyName == "" || !column.IsSortable) + return; // we can't sort by this column + + if (column.IsSortNone) { + // none to ascending + column.Sort = true; + } else if (column.IsSortAscending) { + //ascending to descending + column.Sort = false; + } else { + // descending to none + column.Sort = null; + } + } + }; + + grid.SizeChanged += (s, a) => { + rect.Height = a.NewSize.Height; + Canvas.SetLeft(rect, a.NewSize.Width); + transparentRect.Height = a.NewSize.Height; + Canvas.SetLeft(transparentRect, a.NewSize.Width - TRANSPARENT_WIDTH / 2); + }; + + bool mouseDown = false; + Point initialPos = new Point(0,0); + double initialWidth = 0; + transparentRect.MouseLeftButtonDown += (s, a) => { + mouseDown = true; + initialPos = a.GetPosition(canvas); + initialWidth = ((s as FrameworkElement).DataContext as FastGridViewColumn).Width; + transparentRect.CaptureMouse(); + a.Handled = true; + ((s as FrameworkElement).DataContext as FastGridViewColumn).IsResizingColumn = true; + }; + transparentRect.MouseLeftButtonUp += (s, a) => { + mouseDown = false; + transparentRect.ReleaseMouseCapture(); + a.Handled = true; + ((s as FrameworkElement).DataContext as FastGridViewColumn).IsResizingColumn = false; + }; + transparentRect.MouseMove += (s, a) => { + if (mouseDown && (s as FrameworkElement).DataContext is FastGridViewColumn column) { + var curPos = a.GetPosition(canvas); + var newWidth = initialWidth + (curPos.X - initialPos.X); + if (!double.IsNaN(column.MinWidth)) + newWidth = Math.Max(newWidth, column.MinWidth); + if (!double.IsNaN(column.MaxWidth)) + newWidth = Math.Min(newWidth, column.MaxWidth); + newWidth = Math.Max(newWidth, MIN_WIDTH); + column.Width = newWidth; + } + a.Handled = true; + }; + return grid; + }); + return dt; + } + + private static void Column_PropertyChanged(Grid grid) { + var column = grid?.DataContext as FastGridViewColumn; + if (grid == null || column == null) + return; + + var path = grid.Children.OfType().FirstOrDefault(); + if (column.IsSortAscending) { + (path.RenderTransform as RotateTransform).Angle = 0; + path.Opacity = 1; + } else if (column.IsSortDescending) { + (path.RenderTransform as RotateTransform).Angle = 180; + path.Opacity = 1; + } else { + path.Opacity = 0; + } + } + } +} diff --git a/src/FastControls/FastGrid/FastGridUtil.cs b/src/FastControls/FastGrid/FastGridUtil.cs new file mode 100644 index 0000000..19a9c7a --- /dev/null +++ b/src/FastControls/FastGrid/FastGridUtil.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using OpenSilver.Internal.Xaml; +using OpenSilver.Internal.Xaml.Context; + +namespace FastGrid.FastGrid +{ + internal static class FastGridUtil + { + private const double TOLERANCE = 0.0001; + + public static void SetPropertyViaReflection(object obj, string propertyName, object value) { + var prop = obj.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Debug.Assert(prop != null); + prop.SetValue(obj, value); + } + + public static DataTemplate CreateDataTemplate(Func creator) { + var xamlContext = RuntimeHelpers.Create_XamlContext(); + var dt = new DataTemplate(); + Func factory = (control, xc) => { + var fe = creator(); + RuntimeHelpers.SetTemplatedParent(fe, control); + return fe; + }; + + RuntimeHelpers.SetTemplateContent(dt, xamlContext, factory); + return dt; + } + + public static void SetLeft(FrameworkElement fe, double left) + { + if (Math.Abs(Canvas.GetLeft(fe) - left) > TOLERANCE) + Canvas.SetLeft(fe, left); + } + public static void SetTop(FrameworkElement fe, double top) + { + if (Math.Abs(Canvas.GetTop(fe) - top) > TOLERANCE) + Canvas.SetTop(fe, top); + } + + public static void SetWidth(FrameworkElement fe, double width) + { + if (Math.Abs(fe.Width - width) > TOLERANCE) + fe.Width = width; + } + public static void SetHeight(FrameworkElement fe, double height) + { + if (Math.Abs(fe.Height - height) > TOLERANCE) + fe.Height = height; + } + + public static void SetOpacity(FrameworkElement fe, double opacity) { + if (Math.Abs(fe.Opacity - opacity) > TOLERANCE) + fe.Opacity = opacity; + } + + public static void SetDataContext(FrameworkElement fe, object context) { + if (!ReferenceEquals(fe.DataContext, context)) + fe.DataContext = context; + } + + public static void SetIsVisible(FrameworkElement fe, bool isVisible) { + if ((fe.Visibility == Visibility.Visible) != isVisible) + fe.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed; + } + + public static T TryGetAscendant(FrameworkElement fe) where T : FrameworkElement { + while (fe != null) { + if (fe is T t) + return t; + fe = VisualTreeHelper.GetParent(fe) as FrameworkElement; + } + + return null; + } + + public static Brush ControlBackground(FrameworkElement fe) { + if (fe is Control control) + return control.Background; + else if (fe is Panel panel) + return panel.Background; + else if (fe is Border border) + return border.Background; + return null; + } + + public static void SetControlBackground(FrameworkElement fe, Brush bg) { + if (fe is Control control) + control.Background = bg; + else if (fe is Panel panel) + panel.Background = bg; + else if (fe is Border border) + border.Background = bg; + } + + public static int RefIndex(IReadOnlyList list, T value) { + var idx = 0; + foreach (var i in list) + if (ReferenceEquals(i, value)) + return idx; + else + ++idx; + return -1; + } + + public static bool SameColor(Brush a, Brush b) { + if (a is SolidColorBrush aSolid && b is SolidColorBrush bSolid && aSolid.Color == bSolid.Color) + return true; + + // FIXME care about lineargradientbrush as well + return false; + } + } +} diff --git a/src/FastControls/FastGrid/FastGridView.xaml b/src/FastControls/FastGrid/FastGridView.xaml new file mode 100644 index 0000000..69ddbd6 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridView.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/src/FastControls/FastGrid/FastGridView.xaml.cs b/src/FastControls/FastGrid/FastGridView.xaml.cs new file mode 100644 index 0000000..ef9fa76 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridView.xaml.cs @@ -0,0 +1,1368 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Threading; +using OpenSilver; + +namespace FastGrid.FastGrid +{ + /* + + * filtering + * - basically monitor for changes (based on sort, etc.) + * + * + text wrapping + * + * + * + * + * + * + * IMPORTANT: + * Filtering + * - at this time, we don't monitor objects that don't match the filter, even if a change could actually make them match the filter. + * Example: I can filter by Salary>1000. I add a user with Salary=500 (automatically filtered out). I later set his Salary=1001 (it will still be filtered out) + * Note: this limitation exists in Telerik as well. + * + FIXME + * move to ControlKit + * + * FIXME CellEditTemplate + * + * LATER IDEAS: + * - just in case this is still slow - i can create copies of the original objects and update stuff at a given interval + * (note: for me + .net7 -> this works like a boss, so not needed yet) + * + * */ + + public partial class FastGridView : UserControl, INotifyPropertyChanged { + private const double TOLERANCE = 0.0001; + + public const double SCROLLBAR_WIDTH = 12; + public const double SCROLLBAR_HEIGHT = 12; + + private const int OUTSIDE_SCREEN = -100000; + + // these are the rows - not all of them need to be visible, since we'll always make sure we have enough rows to accomodate the whole height of the control + private List _rows = new List(); + private IReadOnlyList _items; + private bool _suspendRender = false; + + // this is the first visible row - based on this, I re-compute the _topRowIndex, on each update of the UI + // the idea is this: no matter how many insertions/deletions, the top row remains the same -- but the top row index can change + private object _topRow = null; + // what's the top row index? + private int _topRowIndexWhenNotScrolling = 0; + + // if >= 0, we're scrolling to this row + private int _scrollingTopRowIndex = -1; + + // the idea - i don't want new rows to be created while I'm iterating the collection to update rows' positions + private bool _isUpdatingUI = false; + + private int _visibleCount = 0; + + private DispatcherTimer _postponeUiTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100)}; + private DispatcherTimer _checkOffscreenUiTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500)}; + + private FastGridViewColumnCollectionInternal _columns; + + private FastGridViewSortDescriptors _sortDescriptors; + private FastGridViewSort _sort; + + private int uiTimerInterval_ = 100; + + private static bool _firstTime = true; + + private int _successfullyDrawnTopRowIdx = -1;//_topRowIndexWhenNotScrolling + + private bool _needsRefilter = false, _needsFullReSort = false, _needsReSort = false; + + public FastGridView() { + this.InitializeComponent(); + _columns = new FastGridViewColumnCollectionInternal(this); + _sort = new FastGridViewSort(this); + _sortDescriptors = new FastGridViewSortDescriptors(); + _sortDescriptors.OnResort += () => { + _needsFullReSort = true; + PostponeUpdateUI(); + }; + + _postponeUiTimer.Tick += (s, a) => { + if (_scrollingTopRowIndex >= 0) { + _successfullyDrawnTopRowIdx = -1; + if (TryScrollToRowIndex(_scrollingTopRowIndex, out var optimizeDrawNow)) { + // scroll succeeded + if (_scrollingTopRowIndex < SortedItems.Count) { + _topRow = SortedItems[_scrollingTopRowIndex]; + _topRowIndexWhenNotScrolling = _scrollingTopRowIndex; + } else { + _topRow = SortedItems.Count > 0 ? SortedItems[0] : null; + _topRowIndexWhenNotScrolling = 0; + } + _scrollingTopRowIndex = -1; + Console.WriteLine($"new top row (scrolled) {Name}: {_topRowIndexWhenNotScrolling}"); + } + // the idea - allow the new bound rows to be shown visually (albeit, off-screen) + // this way, on the next tick, I can show them to the user visually -- instantly + if (!optimizeDrawNow) + return; + } + + var uiAlreadyDrawn = _successfullyDrawnTopRowIdx == _topRowIndexWhenNotScrolling && _successfullyDrawnTopRowIdx >= 0; + if (uiAlreadyDrawn) { + PreloadAhead(); + _postponeUiTimer.Stop(); + return; + } + + if (TryUpdateUI()) + // the idea - first, I show the user the updated UI (which can be time consuming anyway) + // then, on the next tick, I will preload stuff ahead, so that if user just moves a few rows up/down, everything is already good to go + _successfullyDrawnTopRowIdx = _topRowIndexWhenNotScrolling; + }; + verticalScrollbar.Minimum = 0; + horizontalScrollbar.Minimum = 0; + + _checkOffscreenUiTimer.Tick += (s, a) => { + var left = Canvas.GetLeft(this); + var top = Canvas.GetTop(this); + var isOffscreen = left < -9999 || top < -9999; + IsOffscreen = isOffscreen; + }; + + if (_firstTime) { + _firstTime = false; + Interop.ExecuteJavaScript("document.addEventListener('contextmenu', event => event.preventDefault());"); + } + } + + private int TopRowIndex => _scrollingTopRowIndex >= 0 ? _scrollingTopRowIndex : _topRowIndexWhenNotScrolling; + private bool IsEmpty => SortedItems == null || SortedItems.Count < 1; + internal IReadOnlyList FilteredItems => _items; + internal IReadOnlyList SortedItems => _sort.SortedItems; + + public FastGridViewColumnCollection Columns => _columns; + public FastGridViewSortDescriptors SortDescriptors => _sortDescriptors; + public bool IsScrollingVertically => _scrollingTopRowIndex >= 0; + + // IMPORTANT: this can only be set up once, at loading + public bool AllowMultipleSelection { get; set; } = false; + + // the idea - when doing pageup/pgdown - we'll need to create more rows to first show them off-screen + // creation is time consuming, so lets cache ahead -- thus, pageup/pagedown will be faster + public double CreateExtraRowsAheadPercent { get; set; } = 2.1; + + // these are extra rows that are already bound to the underlying objects, ready to be shown on-screen + // the idea - load a few extra rows on top + on bottom, just in case the user wants to do a bit of scrolling + // (like, with the mouse wheel) + public int ShowAheadExtraRows { get; set; } = 7; + + // if true, I allow several sort columns, if false, one sort column at most + public bool AllowSortByMultipleColumns { get; set; } = true; + + // if true -> you set selection index -> then i compute the selected object + // if false -> you set selection object -> then i compute the selection index + // (in this case, if you move the selection object, the selection index changes) + // + // IMPORTANT: + // you usually don't need to care about this, this will update, based on what you bind (SelectedIndex(es) or SelectedItem(s)) + // however, if you don't bind anything, then you may want to set this specifically to true or false + public bool UseSelectionIndex { get; set; } = false; + + // instead of binding the row background, you can also have a function that is called before each row is shown + // rationale: binding the row background might not be possible, or it may sometimes cause a bit of flicker + public Func RowBackgroundColorFunc { get; set; } = null; + + // if true -> on column resize + horizontal scrolling, the effect is instant + // if false -> we dim the cells and then do the resize once the user finishes (much faster) + public bool InstantColumnResize { get; set; } = false; + + // optimization: you can set this to true when we're offscreen -- in this case, we'll unbind all rows (so that no unnecessary updates take place) + // + // by default, if Parent is Canvas, I will check every 0.5 seconds, and if I can determine we're offscreen, I will automatically set this to true + public bool IsOffscreen { + get => isOffscreen_; + set { + if (value == isOffscreen_) return; + isOffscreen_ = value; + OnPropertyChanged(); + } + } + + private double HorizontalOffset { + get => horizontalOffset_; + set { + if (value.Equals(horizontalOffset_)) return; + horizontalOffset_ = value; + OnPropertyChanged(); + } + } + + private bool IsScrollingHorizontally { + get => isScrollingHorizontally_; + set { + if (value == isScrollingHorizontally_) return; + isScrollingHorizontally_ = value; + OnPropertyChanged(); + } + } + + public event EventHandler SelectionChanged; + + public int UiTimerInterval { + get => uiTimerInterval_; + set { + uiTimerInterval_ = value; + _postponeUiTimer.Interval = TimeSpan.FromMilliseconds(value); + } + } + + // FIXME not implemented yet + // note: not bindable at this time + public bool CanUserReorderColumns { get; set; } = false; + + // note: not bindable at this time + public bool CanUserResizeRows { get; set; } = true; + // note: not bindable at this time + public bool CanUserSortColumns { get; set; } = true; + // note: not bindable at this time + public bool IsFilteringAllowed { get; set; } = false; + + public IEnumerable VisibleRows() => _rows.Where(r => r.IsRowVisible).Select(r => r.RowObject); + + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(FastGridView), + new PropertyMetadata(default(IEnumerable), OnItemsSourceChanged)); + public IEnumerable ItemsSource { + get { return (IEnumerable)GetValue(ItemsSourceProperty); } + set { SetValue(ItemsSourceProperty, value); } + } + + private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + var self = d as FastGridView; + self?.OnItemsSourceChanged(); + } + + // row height - -1 = auto computed + // note: at this time (4th Jan 2023) - we only support fixed row heights (no auto) + public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register( + "RowHeight", typeof(double), typeof(FastGridView), new PropertyMetadata(30d, RowHeightChanged)); + + + public double RowHeight { + get { return (double)GetValue(RowHeightProperty); } + set { SetValue(RowHeightProperty, value); } + } + + public static readonly DependencyProperty HeaderHeightProperty = DependencyProperty.Register( + "HeaderHeight", typeof(double), typeof(FastGridView), new PropertyMetadata(30d, HeaderHeightChanged)); + + public double HeaderHeight { + get { return (double)GetValue(HeaderHeightProperty); } + set { SetValue(HeaderHeightProperty, value); } + } + + private static void RowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + (d as FastGridView).RowHeightChanged(); + } + private static void HeaderHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + (d as FastGridView).HeaderHeightChanged(); + } + + private void RowHeightChanged() { + foreach (var row in _rows) + row.RowHeight = RowHeight; + } + + private void HeaderHeightChanged() { + headerCtrl.Height = HeaderHeight; + } + + // to enumerate selection, regarless of its type: GetSelection() + public static readonly DependencyProperty SelectedIndexProperty = DependencyProperty.Register( + "SelectedIndex", typeof(int), typeof(FastGridView), + new PropertyMetadata(-1, SingleSelectedIndexChanged)); + + + public int SelectedIndex + { + get { return (int)GetValue(SelectedIndexProperty); } + set { SetValue(SelectedIndexProperty, value); } + } + + // to enumerate selection, regarless of its type: GetSelection() + public static readonly DependencyProperty SelectedIndexesProperty = DependencyProperty.Register( + "SelectedIndexes", typeof(ObservableCollection), typeof(FastGridView), + new PropertyMetadata(new ObservableCollection(), SelectedIndexesChanged)); + + + public ObservableCollection SelectedIndexes + { + get { return (ObservableCollection)GetValue(SelectedIndexesProperty); } + set { SetValue(SelectedIndexesProperty, value); } + } + + // to enumerate selection, regarless of its type: GetSelection() + public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( + "SelectedItem", typeof(object), typeof(FastGridView), + new PropertyMetadata(null, SingleSelectedItemChanged)); + + + public object SelectedItem + { + get { return (object)GetValue(SelectedItemProperty); } + set { SetValue(SelectedItemProperty, value); } + } + + // to enumerate selection, regarless of its type: GetSelection() + public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( + "SelectedItems", typeof(ObservableCollection), typeof(FastGridView), + new PropertyMetadata(new ObservableCollection(), SelectedItemsChanged)); + public ObservableCollection SelectedItems + { + get { return (ObservableCollection)GetValue(SelectedItemsProperty); } + set { SetValue(SelectedItemsProperty, value); } + } + + + public static readonly DependencyProperty SelectionBackgroundProperty = DependencyProperty.Register( + "SelectionBackground", typeof(Brush), typeof(FastGridView), new PropertyMetadata(new SolidColorBrush(Colors.DarkGray), SelectedBackgroundChanged)); + public Brush SelectionBackground + { + get { return (Brush)GetValue(SelectionBackgroundProperty); } + set { SetValue(SelectionBackgroundProperty, value); } + } + + + + public static readonly DependencyProperty RowTemplateProperty = DependencyProperty.Register( + "RowTemplate", typeof(DataTemplate), typeof(FastGridView), new PropertyMetadata(FastGridContentTemplate. DefaultRowTemplate(), RowTemplateChanged)); + + + public DataTemplate RowTemplate { + get { return (DataTemplate)GetValue(RowTemplateProperty); } + set { SetValue(RowTemplateProperty, value); } + } + + public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register( + "HeaderTemplate", typeof(DataTemplate), typeof(FastGridView), new PropertyMetadata(FastGridContentTemplate.DefaultHeaderTemplate(), HeaderTemplateChanged)); + + + public DataTemplate HeaderTemplate { + get { return (DataTemplate)GetValue(HeaderTemplateProperty); } + set { SetValue(HeaderTemplateProperty, value); } + } + + private static void SingleSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as FastGridView).SingleSelectedIndexChanged(); + } + private static void SelectedIndexesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as FastGridView).SelectedIndexesChanged(); + } + private static void SingleSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as FastGridView).SingleSelectedItemChanged(); + } + private static void SelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as FastGridView).SelectedItemsChanged(); + } + + private static void SelectedBackgroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as FastGridView).SelectedBackgroundChanged(); + } + private static void RowTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + (d as FastGridView).RowTemplateChanged(); + } + private static void HeaderTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + (d as FastGridView).HeaderTemplateChanged(); + } + + private void SelectedBackgroundChanged() + { + foreach (var row in _rows) + row.SelectedBrush = SelectionBackground; + } + + private void SingleSelectedIndexChanged() + { + if (AllowMultipleSelection) + throw new Exception("can't set SelectedIndex on multi-selection"); + + UseSelectionIndex = true; + UpdateSelection(); + SelectionChanged?.Invoke(this,EventArgs.Empty); + } + + private void SelectedIndexesChanged() + { + if (!AllowMultipleSelection) + throw new Exception("can't set SelectedIndexes on multi-selection"); + + UseSelectionIndex = true; + UpdateSelection(); + SelectionChanged?.Invoke(this,EventArgs.Empty); + } + + private void SingleSelectedItemChanged() + { + if (AllowMultipleSelection) + throw new Exception("can't set SelectedItem on multi-selection"); + + UseSelectionIndex = false; + UpdateSelection(); + SelectionChanged?.Invoke(this,EventArgs.Empty); + } + + private void SelectedItemsChanged() + { + if (!AllowMultipleSelection) + throw new Exception("can't set SelectedItems on multi-selection"); + + UseSelectionIndex = false; + UpdateSelection(); + SelectionChanged?.Invoke(this,EventArgs.Empty); + } + + public IEnumerable GetSelection() { + if (AllowMultipleSelection) { + if (UseSelectionIndex) { + foreach (var idx in SelectedIndexes) + if (idx >= 0 && idx < SortedItems.Count) + yield return SortedItems[idx]; + } else + foreach (var obj in SelectedItems) + yield return obj; + } else { + if (UseSelectionIndex) { + if (SelectedIndex >= 0 && SelectedIndex < SortedItems.Count) + yield return SortedItems[SelectedIndex]; + } else if (SelectedItem != null) + yield return SelectedItem; + } + } + + private void UpdateSelection() { + if (IsScrollingVertically) { + PostponeUpdateUI(); + return; + } + var rowIdx = _topRowIndexWhenNotScrolling; + var needsPostponeUI = false; + for (int i = 0; i < _visibleCount; ++i) { + var row = TryGetRow(rowIdx + i); + if (row != null) { + var obj = SortedItems[rowIdx + i]; + row.IsSelected = IsRowSelected(obj, rowIdx + i); + } else + needsPostponeUI = true; + } + + if (needsPostponeUI) + // perhaps user started scrolling or something, since we could not find a row to update + PostponeUpdateUI(); + } + + private void RowTemplateChanged() { + ClearCanvas(); + PostponeUpdateUI(); + } + + private void HeaderTemplateChanged() { + headerCtrl.ItemTemplate = HeaderTemplate; + } + + private void ClearCanvas() { + // the idea -> all our extra controls are kept in a child canvas (like, scroll bars + header) + _rows.Clear(); + foreach (var child in canvas.Children.ToList()) + if (child is FastGridViewRow) + canvas.Children.Remove(child); + } + + private void OnItemsSourceChanged() { + Console.WriteLine($"fastgrid itemsource set for {Name}"); + if (ItemsSource == null) { + ClearCanvas(); + PostponeUpdateUI(); + return; + } + + // allow only ObservableCollection - fast to iterate + know when elements are added/removed + if (!(ItemsSource is INotifyCollectionChanged) || !(ItemsSource is IReadOnlyList)) + throw new Exception("ItemsSource needs to be ObservableCollection<>"); + + if (_items is INotifyCollectionChanged oldColl) + oldColl.CollectionChanged -= FastGridView_CollectionChanged; + _items = (IReadOnlyList)ItemsSource ; + (ItemsSource as INotifyCollectionChanged).CollectionChanged += FastGridView_CollectionChanged; + + // force recompute + _topRow = null; + + ClearCanvas(); + _needsRefilter = true; + _needsFullReSort = true; + PostponeUpdateUI(); + } + + public void SuspendRender() { + _suspendRender = true; + } + + public void ResumeRender() { + if (!_suspendRender) + return; + _suspendRender = false; + PostponeUpdateUI(); + } + + + private void PostponeUpdateUI() { + _successfullyDrawnTopRowIdx = -1; + if (!_postponeUiTimer.IsEnabled) { + Console.WriteLine($"fastgrid: postponed UI update {Name}"); + _postponeUiTimer.Start(); + } + } + + // the idea: + // a property we're sorting by has changed -- thus, we need to resort + internal void NeedsResort() { + // resorting only happens when we need to redraw, no point in doing it faster, + // since several changes can happen at the same time + _needsReSort = true; + PostponeUpdateUI(); + } + + // if object not found, returns -1 + private int ObjectToRowIndex(object obj, int suggestedFindIndex) + { + if (SortedItems == null || SortedItems.Count < 1) + return -1; // nothing to draw + + if (obj == null) + { + // set the top row now + obj = SortedItems[0]; + suggestedFindIndex = 0; + } + + if (suggestedFindIndex < 0 || suggestedFindIndex >= SortedItems.Count) + suggestedFindIndex = 0; + + // the idea: it's very likely the position hasn't changed. And even if it did, it should be very near by + const int MAX_NEIGHBORHOOD = 10; + if ( ReferenceEquals(SortedItems[suggestedFindIndex], obj)) + return suggestedFindIndex; // top row is the same + + for (int i = 1; i < MAX_NEIGHBORHOOD; ++i) + { + var beforeIdx = suggestedFindIndex - i; + var afterIdx = suggestedFindIndex + i; + if (beforeIdx >= 0 && ReferenceEquals(SortedItems[beforeIdx], obj)) + return beforeIdx; + else if (afterIdx < SortedItems.Count && ReferenceEquals(SortedItems[afterIdx], obj)) + return afterIdx; + } + + // in this case, top row is not in the neighborhood + for (int i = 0; i < SortedItems.Count; ++i) + if (ReferenceEquals(SortedItems[i], obj)) + return i; + + return -1; + } + + private void ComputeTopRowIndex() + { + if (SortedItems == null || SortedItems.Count < 1) { + // nothing to draw + _topRow = null; + _topRowIndexWhenNotScrolling = 0; + return; + } + + var foundIdx = ObjectToRowIndex(_topRow, _topRowIndexWhenNotScrolling); + if (foundIdx == _topRowIndexWhenNotScrolling) + return; // same + + if (foundIdx >= 0) + _topRowIndexWhenNotScrolling = foundIdx; + else { + // if topRow not found -> that means we removed it from the collection. If so, just go to the top + _topRow = SortedItems[0]; + _topRowIndexWhenNotScrolling = 0; + } + Debug.WriteLine($"new top row {Name}: {_topRowIndexWhenNotScrolling}"); + } + + private bool CanDraw() { + if (_columns == null) + return false; // not initialized yet + if (canvas.Width < 1 || canvas.Height < 1 || Visibility == Visibility.Collapsed) + return false; // we're hidden + if (_suspendRender) + return false; + if (SortedItems == null) + return false; + if (RowHeight < 1) + return false; + if (_isUpdatingUI) + return false; + + return true; + } + + // the idea: when you do a pageup/pagedown, without this optimization, it would re-bind all rows (to the newly scrolled data), and that would take time + // (visually, 250ms or so), so the user would actually see all the rows clear, and then redrawn; and for about 250 ms, the rows would appear clear -- not visually appealing + // + // the workaround is to visually load the scrolled rows outside the screen, which will not affect the user in any way. then, when all is created/bound/shown, bring it into the user's view + private bool TryScrollToRowIndex(int rowIdx, out bool optimizeDrawNow) { + optimizeDrawNow = false; + if (!CanDraw()) + return false; + + Console.WriteLine($"scroll to {rowIdx} - started"); + _isUpdatingUI = true; + var newlyCreatedRowCount = 0; + try { + var maxRowIdx = Math.Min(SortedItems.Count, rowIdx + _visibleCount ); + while (rowIdx < maxRowIdx) { + var tryGetRow = TryGetRow(rowIdx); + var tryReuseRow = tryGetRow == null ? TryReuseRow() : null; + var tryCreateRow = tryReuseRow == null && tryGetRow == null ? CreateRow() : null; + + var row = tryGetRow ?? tryReuseRow ?? tryCreateRow; + if (tryGetRow == null) { + // it's a newly created/bound row - create it outside of what the user sees + // once it's fully created + bound, then we can show it visually, and it will be instant + FastGridUtil.SetLeft(row, OUTSIDE_SCREEN); + ++newlyCreatedRowCount; + } + + var obj = SortedItems[rowIdx]; + row.RowObject = obj; + row.Used = true; + row.IsRowVisible = true; + FastGridUtil.SetDataContext(row, SortedItems[rowIdx]); + row.IsSelected = IsRowSelected(obj, rowIdx); + + ++rowIdx; + } + } finally { + _isUpdatingUI = false; + } + Console.WriteLine($"scroll COMPLETE"); + const int MAX_NEWLY_CREATED_ROW_COUNT_IS_INSTANT = 4; + optimizeDrawNow = newlyCreatedRowCount <= MAX_NEWLY_CREATED_ROW_COUNT_IS_INSTANT; + return true; + } + + private bool TryUpdateUI() { + if (IsEmpty) { + FastGridUtil.SetIsVisible(canvas, false); + return false; + } + FastGridUtil.SetIsVisible(canvas, true); + if (!CanDraw()) + return false; + + _isUpdatingUI = true; + try { + if (_needsRefilter) + FullReFilter(); + if (_needsFullReSort) { + _needsFullReSort = false; + _needsReSort = false; + _sort.FullResort(); + } + if (_needsReSort) { + _needsReSort = false; + _sort.Resort(); + } + + ComputeTopRowIndex(); + + double y = HeaderHeight; + var height = canvas.Height; + var rowIdx = _topRowIndexWhenNotScrolling; + + foreach (var row in _rows) + row.IsRowVisible = false; + + var visibleCount = 0; + while (y < height && rowIdx < SortedItems.Count) { + var row = TryGetRow(rowIdx) ?? TryReuseRow() ?? CreateRow(); + FastGridUtil.SetTop(row, y); + Debug.Assert(row.RowHeight >= 0); + var obj = SortedItems[rowIdx]; + row.RowObject = obj; + row.Used = true; + row.IsRowVisible = true; + y += row.RowHeight; + FastGridUtil.SetDataContext(row, SortedItems[rowIdx]); + UpdateRowColor(row); + row.IsSelected = IsRowSelected(obj, rowIdx); + row.HorizontalOffset = HorizontalOffset; + row.UpdateUI(); + + ++rowIdx; + ++visibleCount; + } + // ... update visible count just once, just in case the above is async + _visibleCount = visibleCount; + Console.WriteLine($"fastgrid {Name} - draw {_visibleCount}"); + + HideInvisibleRows(); + UpdateVerticalScrollbar(); + + } finally { + _isUpdatingUI = false; + } + return true; + } + + private void PreloadAhead() { + _isUpdatingUI = true; + List extra = new List(); + for (int i = 1; i <= ShowAheadExtraRows; ++i) { + if (_topRowIndexWhenNotScrolling - i >= 0) + extra.Add(_topRowIndexWhenNotScrolling - i); + if (_topRowIndexWhenNotScrolling + _visibleCount + i < SortedItems.Count) + extra.Add(_topRowIndexWhenNotScrolling + _visibleCount + i); + } + // note: dump only those that haven't already been loaded + Console.WriteLine($"fastgrid {Name} - preloading ahead [{string.Join(",",extra.Where(i => TryGetRow(i) == null))}]"); + var cacheAhead =(int)Math.Round(_visibleCount * CreateExtraRowsAheadPercent + ShowAheadExtraRows * 2) ; + while (_rows.Count < cacheAhead) { + var row = CreateRow(); + row.Used = false; + FastGridUtil.SetLeft(row, OUTSIDE_SCREEN); + } + try { + + foreach (var row in _rows) + row.Preloaded = false; + + foreach (var rowIdx in extra) { + var row = TryGetRow(rowIdx) ?? TryReuseRow() ?? CreateRow(); + FastGridUtil.SetLeft(row, OUTSIDE_SCREEN); + var obj = SortedItems[rowIdx]; + row.RowObject = obj; + row.Used = true; + row.Preloaded = true; + row.IsRowVisible = false; + FastGridUtil.SetDataContext(row, SortedItems[rowIdx]); + UpdateRowColor(row); + row.IsSelected = IsRowSelected(obj, rowIdx); + row.HorizontalOffset = HorizontalOffset; + row.UpdateUI(); + } + HideInvisibleRows(); + } finally { + _isUpdatingUI = false; + } + } + + + private void UpdateRowColor(FastGridViewRow row) { + if (RowBackgroundColorFunc != null) { + var color = RowBackgroundColorFunc(row.RowObject); + + if (!FastGridUtil.SameColor(color, FastGridUtil.ControlBackground(row.RowContentChild))) + FastGridUtil.SetControlBackground(row.RowContentChild, color); + } + } + + private int MaxRowIdx() { + // ... note: the last row is not fully visible + var visibleCount = GuessRowCount(); + var maxRowIdx = Math.Max(SortedItems.Count - visibleCount + 1, 0); + + return maxRowIdx; + } + + private void horiz_bar_scroll(object sender, ScrollEventArgs e) { + var SMALL = 10; + var LARGE = 100; + switch (e.ScrollEventType) { + case ScrollEventType.SmallDecrement: + HorizontalScroll(horizontalScrollbar.Value - SMALL); + break; + case ScrollEventType.SmallIncrement: + HorizontalScroll(horizontalScrollbar.Value + SMALL); + break; + case ScrollEventType.LargeDecrement: + HorizontalScroll(horizontalScrollbar.Value - LARGE); + break; + case ScrollEventType.LargeIncrement: + HorizontalScroll(horizontalScrollbar.Value + LARGE); + break; + case ScrollEventType.ThumbTrack: + IsScrollingHorizontally = true; + HorizontalScroll(e.NewValue, updateScrollbarValue: false); + if (!InstantColumnResize) + SetRowOpacity(0.4); + break; + case ScrollEventType.EndScroll: + IsScrollingHorizontally = false; + HorizontalScroll(e.NewValue, updateScrollbarValue: false); + if (!InstantColumnResize) + SetRowOpacity(1); + break; + case ScrollEventType.ThumbPosition: + HorizontalScroll(e.NewValue); + break; + case ScrollEventType.First: + HorizontalScroll(0); + break; + case ScrollEventType.Last: + HorizontalScroll(horizontalScrollbar.Maximum); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void HorizontalScroll(double value, bool updateScrollbarValue = true) { + value = Math.Max(horizontalScrollbar.Minimum, Math.Min(value, horizontalScrollbar.Maximum)); + if (InstantColumnResize || !IsScrollingHorizontally) + HorizontalOffset = value; + + if (updateScrollbarValue) + horizontalScrollbar.Value = value; + } + + private void SetRowOpacity(double value) { + foreach (var row in _rows.Where(r => r.IsRowVisible)) + FastGridUtil.SetOpacity(row, value); + } + + private void vertical_bar_scroll(object sender, System.Windows.Controls.Primitives.ScrollEventArgs e) + { + // ... note: the last row is not fully visible + var visibleCount = Math.Max( _visibleCount - 1 , 0); + var maxRowIdx = MaxRowIdx(); + var valueScroll = Math.Min((int)e.NewValue, maxRowIdx); + + + switch (e.ScrollEventType) { + case ScrollEventType.SmallDecrement: + if (TopRowIndex > 0) + VerticalScrollToRowIndex(TopRowIndex - 1); + break; + case ScrollEventType.SmallIncrement: + if (TopRowIndex < maxRowIdx) + VerticalScrollToRowIndex(TopRowIndex + 1); + break; + case ScrollEventType.LargeDecrement: + var pageupIdx = Math.Max(TopRowIndex - visibleCount, 0); + VerticalScrollToRowIndex(pageupIdx); + break; + case ScrollEventType.LargeIncrement: + var pagedownIdx = Math.Min(TopRowIndex + visibleCount, maxRowIdx); + VerticalScrollToRowIndex(pagedownIdx); + break; + case ScrollEventType.ThumbTrack: + // this is the user dragging the thumb - I don't want instant update, since very likely that would be very costly + SetRowOpacity(0.4); + break; + case ScrollEventType.EndScroll: + VerticalScrollToRowIndex(valueScroll); + break; + case ScrollEventType.ThumbPosition: + VerticalScrollToRowIndex(valueScroll); + break; + case ScrollEventType.First: + VerticalScrollToRowIndex(0); + break; + case ScrollEventType.Last: + VerticalScrollToRowIndex(maxRowIdx); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void UpdateHorizontalScrollbar() { + // just so the user can clearly see the last column, and also resize it + var EXTRA_SIZE = 20; + + horizontalScrollbar.ViewportSize = canvas.Width; + var columnsWidth = Columns.Sum(c => c.IsVisible ? c.Width : 0); + horizontalScrollbar.Maximum = Math.Max(columnsWidth + EXTRA_SIZE - canvas.Width, 0) ; + } + + private void UpdateVerticalScrollbar() + { + // ... note: the last row is not fully visible + var visibleCount = GuessRowCount(); + if (Math.Abs((double)(verticalScrollbar.ViewportSize - visibleCount)) > TOLERANCE) + verticalScrollbar.ViewportSize = visibleCount; + if (Math.Abs((double)(verticalScrollbar.Value - TopRowIndex)) > TOLERANCE) + verticalScrollbar.Value = TopRowIndex; + + var maxRowIdx = MaxRowIdx(); + if (Math.Abs((double)(verticalScrollbar.Maximum - maxRowIdx)) > TOLERANCE) + verticalScrollbar.Maximum = maxRowIdx; + } + + private bool IsRowSelected(object row, int rowIdx) + { + if (UseSelectionIndex) + { + if (AllowMultipleSelection) + return SelectedIndexes.Contains(rowIdx); + else + return SelectedIndex == rowIdx; + } + else + { + if (AllowMultipleSelection) + return SelectedItems.Any(i => ReferenceEquals(i, row)); + else + return ReferenceEquals(row, SelectedItem); + } + } + + public void VerticalScrollToRowIndex(int rowIdx) { + if (rowIdx < 0 || rowIdx >= SortedItems.Count) + return; // invalid index + + if (_scrollingTopRowIndex >= 0) { + // we're in the process of scrolling already + // (note: while scrolling, the postponeUiTimer is already running) + _scrollingTopRowIndex = rowIdx; + Console.WriteLine($"scroll to row {rowIdx}"); + return; + } + + // here, we're not scrolling + if (!ReferenceEquals(_topRow, SortedItems[rowIdx])) { + _scrollingTopRowIndex = rowIdx; + Console.WriteLine($"scroll to row {rowIdx}"); + PostponeUpdateUI(); + } + } + + public void ScrollToRow(object obj) { + if (SortedItems.Count < 1) + return; // nothing to scroll to + + if (_scrollingTopRowIndex >= 0) { + // we're in the process of scrolling already + // (note: while scrolling, the postponeUiTimer is already running) + _scrollingTopRowIndex = ObjectToRowIndex(obj, _scrollingTopRowIndex); + return; + } + + // here, we're not scrolling + if (ReferenceEquals(_topRow, obj)) + return; + + var rowIdx = ObjectToRowIndex(obj, -1); + if (rowIdx < 0) + rowIdx = 0; + _scrollingTopRowIndex = rowIdx; + Console.WriteLine($"scroll to row {rowIdx}"); + PostponeUpdateUI(); + } + + + private void HideInvisibleRows() { + foreach (var row in _rows) { + var visible = row.IsRowVisible; + var left = visible ? 0 : -100000; + FastGridUtil.SetLeft(row, left); + + if (visible) + FastGridUtil.SetOpacity(row, 1); + else { + if (!row.Preloaded) { + row.DataContext = null; + row.Used = false; + } + } + } + } + + private FastGridViewRow TryGetRow(int rowIdx) + { + var obj = SortedItems[rowIdx]; + foreach (var row in _rows) + if (ReferenceEquals(row.RowObject,obj) && row.Used) + return row; + return null; + } + + private FastGridViewRow TryReuseRow() { + foreach (var row in _rows) + if (row.DataContext == null && !row.Used) { + row.Used = true; + return row; + } + + return null; + } + + private FastGridViewRow CreateRow() { + var row = new FastGridViewRow(RowTemplate, _columns, RowHeight) { + Width = canvas.Width, + Used = true, + SelectedBrush = SelectionBackground, + }; + _rows.Add(row); + canvas.Children.Add(row); + Console.WriteLine($"row created({Name}), rows={_rows.Count}"); + return row; + } + + private void FastGridView_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + if (CanUserSortColumns || IsFilteringAllowed) { + switch (e.Action) { + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Remove: + if (e.NewItems != null) + foreach (var item in e.NewItems) + OnAddedItem(item); + if (e.OldItems != null) + foreach(var item in e.OldItems) + OnRemovedItem(item); + break; + + // consider this is a complete collection reset + default: + if (IsFilteringAllowed) + _needsRefilter = true; + if (CanUserSortColumns) + _needsFullReSort = true; + break; + } + } + + PostponeUpdateUI(); + } + + private bool MatchesFilter(object item) { + if (!IsFilteringAllowed) + return true; // no filtering + + // FIXME + return true; + } + + private void OnAddedItem(object item) { + if (!MatchesFilter(item)) + return; // doesn't matter + _sort.SortedAdd(item); + } + + private void OnRemovedItem(object item) { + if (!MatchesFilter(item)) + return; // doesn't matter + _sort.Remove(item); + } + + private int GuessRowCount() => (int)( canvas.Height / RowHeight); + + private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) { + canvas.Width = e.NewSize.Width; + canvas.Height = e.NewSize.Height; + verticalScrollbar.Width = SCROLLBAR_WIDTH; + verticalScrollbar.Height = Math.Max(e.NewSize.Height - SCROLLBAR_HEIGHT, 0) ; + horizontalScrollbar.Width = Math.Max(e.NewSize.Width - SCROLLBAR_WIDTH, 0) ; + horizontalScrollbar.Height = SCROLLBAR_HEIGHT; + FastGridUtil.SetLeft(verticalScrollbar, e.NewSize.Width - SCROLLBAR_WIDTH); + FastGridUtil.SetTop(horizontalScrollbar, e.NewSize.Height - SCROLLBAR_HEIGHT); + + headerCtrl.Width = e.NewSize.Width; + headerCtrl.Height = HeaderHeight; + + foreach (var row in _rows) + row.Width = e.NewSize.Width; + + UpdateHorizontalScrollbar(); + PostponeUpdateUI(); + } + + private void CreateHeader() { + headerCtrl.ItemTemplate = HeaderTemplate; + headerCtrl.ItemsSource = _columns; + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + CreateHeader(); + + HeaderHeightChanged(); + RowHeightChanged(); + + PostponeUpdateUI(); + } + + public void Redraw() { + PostponeUpdateUI(); + } + + private void SelectAdd(FastGridViewRow row) { + if (UseSelectionIndex) { + var rowIdx = ObjectToRowIndex(row.RowObject, TopRowIndex); + if (rowIdx < 0) + throw new Exception($"fastgrid {Name}: Can't find row"); + if (AllowMultipleSelection) { + var copy = SelectedIndexes.ToList(); + copy.Add(rowIdx); + SelectedIndexes = new ObservableCollection(copy); + } else + SelectedIndex = rowIdx; + } else { + if (AllowMultipleSelection) { + var copy = SelectedItems.ToList(); + copy.Add(row.RowObject); + SelectedItems = new ObservableCollection(copy); + } + else + SelectedItem = row.RowObject; + } + } + private void SelectSet(FastGridViewRow row) { + if (UseSelectionIndex) { + var rowIdx = ObjectToRowIndex(row.RowObject, TopRowIndex); + if (rowIdx < 0) + throw new Exception($"fastgrid {Name}: Can't find row"); + if (AllowMultipleSelection) + SelectedIndexes = new ObservableCollection { rowIdx }; + else + SelectedIndex = rowIdx; + } else { + if (AllowMultipleSelection) { + SelectedItems = new ObservableCollection { row.RowObject }; + } else + SelectedItem = row.RowObject; + } + } + + internal void OnMouseLeftButtonDown(FastGridViewRow row, MouseButtonEventArgs eventArgs) { + row.IsSelected = true; + var ctrl = (eventArgs.KeyModifiers & ModifierKeys.Control) != 0; + if (ctrl) + SelectAdd(row); + else + SelectSet(row); + PostponeUpdateUI(); + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) { + var parent = VisualTreeHelper.GetParent(this); + if (parent is Canvas) + _checkOffscreenUiTimer.Start(); + + HandleFilterSortColumns(); + CreateHeader(); + HandleContextMenu(); + + HeaderHeightChanged(); + RowHeightChanged(); + PostponeUpdateUI(); + } + + private void HandleFilterSortColumns() { + if (!CanUserSortColumns && !IsFilteringAllowed) + return; + + // here, each sortable/filterable column needs to have a databinding property name + foreach (var col in Columns) + if ((col.IsFilterable || col.IsSortable) && col.DataBindingPropertyName == "") + throw new Exception($"Fastgrid: if filter and/or sort, you need to set DataBindingPropertyName for all columns ({col.FriendlyName()})"); + + + } + + // recomputes the filter completely, ignoring any previous cache + // after this, you need to re-sort + private void FullReFilter() { + _needsRefilter = false; + // FIXME to implement + // also - if no filter, reuse _items + _needsFullReSort = true; + } + + private void HandleContextMenu() { + // handle OS bug - when you click on an menu item, by default, it doesn't close + if (ContextMenu != null) { + foreach (var mi in ContextMenu.Items.OfType()) + mi.Click += (s, a) => ContextMenu.IsOpen = false; + } + } + + private void UserControl_Unloaded(object sender, RoutedEventArgs e) { + _postponeUiTimer.Stop(); + _checkOffscreenUiTimer.Stop(); + } + + private void canvas_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) { + if (e.Delta == 0) + return; + Console.WriteLine($"mouse wheel {e.Delta}"); + + var goUp = e.Delta > 0; + var maxRowIdx = MaxRowIdx(); + var newIdx = goUp ? TopRowIndex - 1 : TopRowIndex + 1; + if (newIdx >= 0 && newIdx <= maxRowIdx) + VerticalScrollToRowIndex(newIdx); + } + + internal void OnColumnsCollectionChanged() { + + } + + private bool _ignoreSort = false; + private double horizontalOffset_ = 0; + private bool isScrollingHorizontally_ = false; + private bool isOffscreen_ = false; + + internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyName) { + switch (propertyName) { + case "IsVisible": + foreach (var row in _rows) + row.SetCellVisible(col, col.IsVisible); + PostponeUpdateUI(); + break; + + case "IsResizingColumn": + if (col.IsResizingColumn) { + if (!InstantColumnResize) + SetRowOpacity(0.4); + } else { + if (!InstantColumnResize) { + SetRowOpacity(1); + foreach (var row in _rows) + row.SetCellWidth(col, col.Width); + PostponeUpdateUI(); + } + + UpdateHorizontalScrollbar(); + // the idea - the old value might have become invalid + HorizontalScroll(horizontalScrollbar.Value); + } + break; + + case "Width": + var updateCellWidthNow = InstantColumnResize || !col.IsResizingColumn; + if (updateCellWidthNow) { + foreach (var row in _rows) + row.SetCellWidth(col, col.Width); + PostponeUpdateUI(); + } + break; + + case "MinWidth": + foreach (var row in _rows) + row.SetCellMinWidth(col, col.MinWidth); + PostponeUpdateUI(); + break; + + case "MaxWidth": + foreach (var row in _rows) + row.SetCellMaxWidth(col, col.MaxWidth); + PostponeUpdateUI(); + break; + + case "ColumnIndex": + foreach (var row in _rows) + row.SetCellIndex(col, col.ColumnIndex); + PostponeUpdateUI(); + break; + + case "Sort": + if (!_ignoreSort) { + _ignoreSort = true; + if (!AllowSortByMultipleColumns && !col.IsSortNone) { + var isSameSort = _sortDescriptors.Count == 1 && ReferenceEquals(_sortDescriptors.Columns[0].Column, col); + if (!isSameSort) + _sortDescriptors.Clear(); + } + + if (!col.IsSortNone) + _sortDescriptors.Add(new FastGridSortDescriptor { + Column = col, + SortDirection = col.IsSortAscending ? SortDirection.Ascending : SortDirection.Descending + }); + else + _sortDescriptors.Remove(new FastGridSortDescriptor { Column = col}); + _ignoreSort = false; + } + break; + + case "HeaderText": break; + case "CanResize": break; + case "CellTemplate": break; + case "CellEditTemplate": break; + } + } + + private void OnHorizontalOffsetChange() { + foreach (var row in _rows.Where(r => r.IsRowVisible)) + row.HorizontalOffset = HorizontalOffset; + UpdateHorizontalScrollbar(); + FastGridUtil.SetLeft(headerCtrl, -HorizontalOffset); + } + + private void OnOffscreenChange() { + Console.WriteLine($"fastgrid: {Name} : moved {(isOffscreen_ ? "OFF screen" : "ON screen")}"); + if (IsOffscreen) + foreach (var row in _rows) { + row.DataContext = null; + row.Used = false; + row.RowObject = null; + } + else { + // coming back onscreen - paint ASAP + TryUpdateUI(); + PostponeUpdateUI(); + } + } + + private void vm_PropertyChanged(string name) { + switch (name) { + case "HorizontalOffset": + OnHorizontalOffsetChange(); + break; + case "IsScrollingHorizontally": + if (!IsScrollingHorizontally) + // end of scroll + HorizontalOffset = horizontalScrollbar.Value; + break; + case "IsOffscreen": + OnOffscreenChange(); + break; + } + } + + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + vm_PropertyChanged(propertyName); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewCell.cs b/src/FastControls/FastGrid/FastGridViewCell.cs new file mode 100644 index 0000000..48d0746 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewCell.cs @@ -0,0 +1,75 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewCell : ContentControl, INotifyPropertyChanged + { + private int cellIndex_ = 0; + private bool isCellVisible_ = true; + + // just move it offscreen + public bool IsCellVisible { + get => isCellVisible_; + set { + if (value == isCellVisible_) return; + isCellVisible_ = value; + OnPropertyChanged(); + } + } + + public int CellIndex { + get => cellIndex_; + set { + if (value == cellIndex_) return; + cellIndex_ = value; + OnPropertyChanged(); + } + } + + public FastGridViewCell() { + CustomLayout = true; + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + DataContextChanged += FastGridViewCell_DataContextChanged; + } + + private void FastGridViewCell_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { + var childCount = VisualTreeHelper.GetChildrenCount(this); + if (childCount < 1) + return; + var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter; + if (cp == null) + return; + cp.CustomLayout = true; + cp.HorizontalAlignment = HorizontalAlignment.Stretch; + cp.VerticalAlignment = VerticalAlignment.Stretch; + cp.DataContext = DataContext; + } + + public override void OnApplyTemplate() { + base.OnApplyTemplate(); + var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter; + cp.CustomLayout = true; + cp.HorizontalAlignment = HorizontalAlignment.Stretch; + cp.VerticalAlignment = VerticalAlignment.Stretch; + cp.DataContext = DataContext; + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) { + // later: needed for CellEditTemplate + base.OnMouseLeftButtonDown(eventArgs); + } + + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewColumn.cs b/src/FastControls/FastGrid/FastGridViewColumn.cs new file mode 100644 index 0000000..c95bafa --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewColumn.cs @@ -0,0 +1,190 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; + +namespace FastGrid.FastGrid +{ + public class FastGridViewColumn : DependencyObject, INotifyPropertyChanged + { + + public static readonly DependencyProperty WidthProperty = DependencyProperty.Register( + "Width", typeof(double), typeof(FastGridViewColumn), + new PropertyMetadata(default(double), (d,_) => OnPropertyChanged(d,"Width"))); + + public double Width { + get { return (double)GetValue(WidthProperty); } + set { SetValue(WidthProperty, value); } + } + + public static readonly DependencyProperty MinWidthProperty = DependencyProperty.Register( + "MinWidth", typeof(double), typeof(FastGridViewColumn), new PropertyMetadata(double.NaN, (d,_) => OnPropertyChanged(d,"MinWidth"))); + + public double MinWidth { + get { return (double)GetValue(MinWidthProperty); } + set { SetValue(MinWidthProperty, value); } + } + + public static readonly DependencyProperty MaxWidthProperty = DependencyProperty.Register( + "MaxWidth", typeof(double), typeof(FastGridViewColumn), new PropertyMetadata(double.NaN, (d,_) => OnPropertyChanged(d,"MaxWidth"))); + + public double MaxWidth { + get { return (double)GetValue(MaxWidthProperty); } + set { SetValue(MaxWidthProperty, value); } + } + + public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.Register( + "HeaderText", typeof(string), typeof(FastGridViewColumn), new PropertyMetadata("", (d,_) => OnPropertyChanged(d,"HeaderText"))); + // simple for now + public string HeaderText { + get { return (string)GetValue(HeaderTextProperty); } + set { SetValue(HeaderTextProperty, value); } + } + + public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register( + "IsVisible", typeof(bool), typeof(FastGridViewColumn), new PropertyMetadata(true, (d,_) => OnPropertyChanged(d,"IsVisible"))); + + public bool IsVisible { + get { return (bool)GetValue(IsVisibleProperty); } + set { SetValue(IsVisibleProperty, value); } + } + + public static readonly DependencyProperty CanResizeProperty = DependencyProperty.Register( + "CanResize", typeof(bool), typeof(FastGridViewColumn), new PropertyMetadata(true, (d,_) => OnPropertyChanged(d,"CanResize"))); + + public bool CanResize { + get { return (bool)GetValue(CanResizeProperty); } + set { SetValue(CanResizeProperty, value); } + } + + // key in the collection -- allow access via string key + public static readonly DependencyProperty UniqueNameProperty = DependencyProperty.Register( + "UniqueName", typeof(string), typeof(FastGridViewColumn), new PropertyMetadata("")); + + public string UniqueName { + get { return (string)GetValue(UniqueNameProperty); } + set { SetValue(UniqueNameProperty, value); } + } + + // future: + // this is how we're ordering the columns -> at this time, doesn't fully work + public static readonly DependencyProperty ColumnIndexProperty = DependencyProperty.Register( + "ColumnIndex", typeof(int), typeof(FastGridViewColumn), new PropertyMetadata(-1)); + + public int ColumnIndex { + get { return (int)GetValue(ColumnIndexProperty); } + set { SetValue(ColumnIndexProperty, value); } + } + + internal bool IsResizingColumn { + get => isResizingColumn_; + set { + if (value == isResizingColumn_) return; + isResizingColumn_ = value; + OnPropertyChanged(); + } + } + + // true -> ascending, false -> descending, null -> none + public bool? Sort { + get => sort_; + set { + if (value == sort_) return; + sort_ = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsSortAscending)); + OnPropertyChanged(nameof(IsSortDescending)); + OnPropertyChanged(nameof(IsSortNone)); + OnPropertyChanged(nameof(SortArrowAngle)); + } + } + + public bool IsSortAscending => Sort == true; + public bool IsSortDescending => Sort == false; + public bool IsSortNone => Sort == null; + public double SortArrowAngle => IsSortAscending ? 0 : 180; + + // note: not bindable at this time + public bool IsFilterable { get; set; } = true; + // note: not bindable at this time + public bool IsSortable { get; set; } = true; + + // the idea: this is the name of the underlying property for this column. You only need to set this + // if you want sorting and/or filtering + // + // note: not bindable at this time + public string DataBindingPropertyName { get; set; } = ""; + public IComparable DataBindingSort { get; set; } = null; + public FastGridViewFilter DataBindingFilter { get; set; } = null; + + public string FriendlyName() => UniqueName != "" ? UniqueName : ColumnIndex.ToString(); + + private static DataTemplate DefaultDataTemplate() { + var dt = FastGridUtil.CreateDataTemplate(() => new Canvas()); + return dt; + } + + private static void OnPropertyChanged(DependencyObject d, string propertyName) { + (d as FastGridViewColumn).OnPropertyChanged(propertyName); + } + public static readonly DependencyProperty CellEditTemplateProperty = + DependencyProperty.Register("CellEditTemplate", typeof(DataTemplate), typeof(FastGridViewColumn), + new PropertyMetadata(DefaultDataTemplate(), (d, _) => OnPropertyChanged(d, "CellEditTemplate"))); + /// + /// Gets or sets the data template for the cell in edit mode. + /// + /// + /// + /// Please refer to for more information on the property. + /// + /// + public DataTemplate CellEditTemplate + { + get + { + return (DataTemplate)this.GetValue(CellEditTemplateProperty); + } + set + { + this.SetValue(CellEditTemplateProperty, value); + } + } + + /// + /// Identifies the CellTemplate property. + /// + public static readonly DependencyProperty CellTemplateProperty = + DependencyProperty.Register("CellTemplate", typeof(DataTemplate), typeof(FastGridViewColumn), + new PropertyMetadata(DefaultDataTemplate(), (d, _) => OnPropertyChanged(d, "CellTemplate"))); + + private bool? sort_ = null; + private bool isResizingColumn_ = false; + + /// + /// Gets or sets the data template for the cell in view mode. + /// + /// + /// + /// Please refer to for more information on the property. + /// + /// + public DataTemplate CellTemplate + { + get + { + return (DataTemplate)this.GetValue(CellTemplateProperty); + } + set + { + this.SetValue(CellTemplateProperty, value); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewColumnCollection.cs b/src/FastControls/FastGrid/FastGridViewColumnCollection.cs new file mode 100644 index 0000000..34f1e70 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewColumnCollection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.ObjectModel; + +namespace FastGrid.FastGrid +{ + public class FastGridViewColumnCollection : ObservableCollection + { + public FastGridViewColumn this[string name] { + get { + foreach (var col in this) { + if (col.UniqueName == "" && col.DataBindingPropertyName == name) + return col; + + if (col.UniqueName == name) + return col; + } + throw new Exception($"column {name} not found"); + } + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs b/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs new file mode 100644 index 0000000..63e553d --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewColumnCollectionInternal : FastGridViewColumnCollection { + private FastGridView _self; + + private List _oldColumns = new List(); + + public FastGridViewColumnCollectionInternal(FastGridView self) { + _self = self; + CollectionChanged += FastGridViewColumnCollectionInternal_CollectionChanged; + Subscribe(); + } + + private void FastGridViewColumnCollectionInternal_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { + Unsubscribe(); + _oldColumns = this.ToList(); + Subscribe(); + _self.OnColumnsCollectionChanged(); + } + + private void Unsubscribe() { + foreach (var col in _oldColumns) + col.PropertyChanged -= Col_PropertyChanged; + } + + private void Col_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + _self.OnColumnPropertyChanged(sender as FastGridViewColumn, e.PropertyName); + } + + private void Subscribe() { + foreach (var col in _oldColumns) + col.PropertyChanged += Col_PropertyChanged; + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewFilter.cs b/src/FastControls/FastGrid/FastGridViewFilter.cs new file mode 100644 index 0000000..3cd1ee5 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewFilter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace FastGrid.FastGrid +{ + public class FastGridViewFilter { + private List _filterItems = new List(); + + public IReadOnlyList FilterItems => _filterItems; + + public void AddFilter(FastGridViewFilterItem filterItem) { + _filterItems.Add(filterItem); + } + + public IReadOnlyList GetFilterList(IReadOnlyList items, FastGridViewColumn col) + { + return null; + } + + public bool Matches(object obj) { + if (FilterItems.Count == 0) + return true; + + return FilterItems.All(f => f.Matches(obj)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewFilterItem.cs b/src/FastControls/FastGrid/FastGridViewFilterItem.cs new file mode 100644 index 0000000..6e9e1f1 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewFilterItem.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; + +namespace FastGrid.FastGrid +{ + public class FastGridViewFilterItem : INotifyPropertyChanged + { + public string PropertyName { + get => propertyName_; + set { + if (value == propertyName_) return; + propertyName_ = value; + OnPropertyChanged(); + } + } + + public object PropertyValue { + get => propertyValue_; + set { + if (Equals(value, propertyValue_)) return; + propertyValue_ = value; + OnPropertyChanged(); + } + } + + public CompareType Compare { + get => compare_; + set { + if (value == compare_) return; + compare_ = value; + OnPropertyChanged(); + } + } + + // ... just in case we end up comparing doubles + public double Tolerance { get; set; } = 0.0000001; + + private PropertyInfo _propertyInfo; + private object propertyValue_ = null; + private string propertyName_ = ""; + private CompareType compare_ = CompareType.Equal; + + public enum CompareType { + Equal, Different, Less, Bigger, LessOrEqual, BiggerOrEqual, + StartsWith, EndsWith, Contains, + } + + public bool Matches(object obj) { + if (_propertyInfo == null && PropertyName != "") { + _propertyInfo = obj.GetType().GetProperty(PropertyName, BindingFlags.Public | BindingFlags.Instance); + } + + if (_propertyInfo == null) + return true; // could not get property? + + var objValue = _propertyInfo.GetValue(obj); + return MatchesValue(objValue, PropertyValue); + } + + private static double? TryGetDouble(object a) { + if (a is double) + return (double)a; + if (a is float) + return (float)a; + + return null; + } + private static long? TryGetInteger(object a) { + if (a is int) + return (int)a; + if (a is short) + return (short)a; + if (a is long) + return (long)a; + if (a is byte) + return (byte)a; + if (a is char) + return (char)a; + return null; + } + private static ulong? TryGetUnsignedInteger(object a) { + if (a is ushort) + return (ushort)a; + if (a is uint) + return (uint)a; + if (a is ulong) + return (ulong)a; + return null; + } + private DateTime? TryGetDate(object a) { + if (a is DateTime) + return (DateTime)a; + return null; + } + + private bool MatchesComparison(bool equal, bool less) { + switch (Compare) { + case CompareType.Equal: + return equal; + case CompareType.Different: + return !equal; + case CompareType.Less: + return less; + case CompareType.Bigger: + return !less && !equal; + case CompareType.LessOrEqual: + return less || equal; + case CompareType.BiggerOrEqual: + return equal || !less; + + case CompareType.StartsWith: + case CompareType.EndsWith: + case CompareType.Contains: + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private bool MatchesDouble(object a, object b) { + var doubleA = TryGetDouble(a); + var doubleB = TryGetDouble(b); + + if (doubleA == null) { + var intA = TryGetInteger(a); + var uintA = TryGetUnsignedInteger(a); + if (intA != null) + doubleA = intA; + else if (uintA != null) + doubleA = uintA; + } + + if (doubleB == null) { + var intB = TryGetInteger(b); + var uintB = TryGetUnsignedInteger(b); + if (intB != null) + doubleB = intB; + else if (uintB != null) + doubleB = uintB; + } + + if (doubleA == null || doubleB == null) + return false; // one could convert to double, one could not + var equal = Math.Abs(doubleA.Value - doubleB.Value) < Tolerance; + var less = doubleA.Value < doubleB.Value; + return MatchesComparison(equal, less); + } + private bool MatchesUlong(object a, object b) { + var intA = TryGetUnsignedInteger(a); + var intB = TryGetUnsignedInteger(b); + if (intA == null || intB == null) + return false; + + var equal = intA.Value == intB.Value; + var less = intA.Value < intB.Value; + return MatchesComparison(equal, less); + } + private bool MatchesLong(object a, object b) { + var intA = TryGetInteger(a); + var intB = TryGetInteger(b); + if (intA == null || intB == null) + return false; + + var equal = intA.Value == intB.Value; + var less = intA.Value < intB.Value; + return MatchesComparison(equal, less); + } + + private bool MatchesDate(object a, object b) { + var dateA = TryGetDate(a); + var dateB = TryGetDate(b); + if (dateA == null || dateB == null) + return false; + var equal = dateA.Value == dateB.Value; + var less = dateA.Value == dateB.Value; + return MatchesComparison(equal, less); + } + + private bool MatchesString(object a, object b) { + var stringA = a.ToString(); + var stringB = b.ToString(); + + switch (Compare) { + case CompareType.StartsWith: + return stringA.StartsWith(stringB); + case CompareType.EndsWith: + return stringA.EndsWith(stringB); + case CompareType.Contains: + return stringA.IndexOf(stringB) >= 0; + } + + var compare = String.Compare(stringA, stringB, StringComparison.Ordinal); + var equal = compare == 0; + var less = compare < 0; + return MatchesComparison(equal, less); + } + + private bool MatchesValue(object objValue, object propertyValue) { + if (objValue == null || propertyValue == null) + return objValue == null && propertyValue == null; + + if (TryGetDouble(objValue) != null || TryGetDouble(propertyValue) != null) + return MatchesDouble(objValue, propertyValue); + if (TryGetInteger(objValue) != null || TryGetInteger(propertyValue) != null) + return MatchesLong(objValue, propertyValue); + if (TryGetUnsignedInteger(objValue) != null || TryGetUnsignedInteger(propertyValue) != null) + return MatchesUlong(objValue, propertyValue); + if (TryGetDate(objValue) != null || TryGetDate(propertyValue) != null) + return MatchesDate(objValue, propertyValue); + // long ulong double + // datetime + + // string + // object - convert to string + return MatchesString(objValue, propertyValue); + } + + + private void vm_propertyChanged(string name) { + _propertyInfo = null; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + vm_propertyChanged(propertyName); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewRow.cs b/src/FastControls/FastGrid/FastGridViewRow.cs new file mode 100644 index 0000000..1ad3416 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewRow.cs @@ -0,0 +1,264 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewRow : Canvas, INotifyPropertyChanged { + + private IReadOnlyList _columns; + private List _cells = new List(); + private bool _loaded = false; + private double _rowHeight = 0; + + internal bool Used = false; + internal bool Preloaded = false; + private bool _isSelected = false; + private Brush _selectedBrush; + + private DataTemplate _rowTemplate; + private FastGridViewRowContent _content; + // the only reason for this is to visually show the selection, ON TOP of the _content + private Canvas _selection; + + private Brush _transparent = new SolidColorBrush(Colors.Transparent); + private object _rowObject = null; + + private FrameworkElement _rowContentChild; + private double horizontalOffset_ = 0; + + internal FastGridViewRowContent RowContent => _content; + internal FrameworkElement RowContentChild { + get { + if (_rowContentChild == null) { + if (VisualTreeHelper.GetChildrenCount(_content) > 0) { + var child = VisualTreeHelper.GetChild(_content, 0) as ContentPresenter; + var grandChild = VisualTreeHelper.GetChild(child, 0) as FrameworkElement; + _rowContentChild = grandChild; + } + } + + return _rowContentChild; + } + } + + public FastGridViewRow(DataTemplate rowTemplate, IReadOnlyList columnInfo, double rowHeight) { + CustomLayout = true; + _columns = columnInfo; + RowHeight = rowHeight; + _rowTemplate = rowTemplate; + Load(); + _loaded = true; + BackgroundChanged(); + UpdateUI(); + + // to catch the mouse anywhere + Background = _transparent; + SizeChanged += FastGridViewRow_SizeChanged; + } + + private void FastGridViewRow_SizeChanged(object sender, SizeChangedEventArgs e) { + _content.Width = e.NewSize.Width; + _content.Height = e.NewSize.Height; + _selection.Width = e.NewSize.Width; + _selection.Height = e.NewSize.Height; + } + + private void Load() { + Height = _rowHeight; + HorizontalAlignment = HorizontalAlignment.Left; + VerticalAlignment = VerticalAlignment.Top; + + // content is added first + _content = new FastGridViewRowContent + { + ContentTemplate = _rowTemplate, + Height = _rowHeight, + }; + Children.Add(_content); + _selection = new Canvas(); + Children.Add(_selection); + + // create the cells + var offset = 0d; + foreach (var ci in _columns) { + var cc = new FastGridViewCell { + ContentTemplate = ci.CellTemplate, + IsCellVisible = ci.IsVisible, + CellIndex = ci.ColumnIndex, + Width = ci.Width, + MinWidth = ci.MinWidth, + MaxWidth = ci.MaxWidth, + Height = _rowHeight, + }; + offset += ci.Width; + _cells.Add(cc); + Children.Add(cc); + } + } + + // the idea for keeping the object instead of the index: + // to easily handle insertions / deletions (in which case, the RowIndex could change for each of the rows) + public object RowObject + { + get => _rowObject; + set + { + // INTENTIONAL - use ReferenceEquals instead of Equals + if (ReferenceEquals(value, _rowObject)) + return; + _rowObject = value; + OnPropertyChanged(); + } + } + + public bool IsRowVisible { get; set; } = false; + + public double RowHeight { + get => _rowHeight; + set { + if (value.Equals(_rowHeight)) return; + _rowHeight = value; + OnPropertyChanged(); + } + } + + public Brush SelectedBrush + { + get => _selectedBrush; + set + { + if (Equals(value, _selectedBrush)) return; + _selectedBrush = value; + OnPropertyChanged(); + } + } + + public bool IsSelected + { + get => _isSelected; + set + { + if (value == _isSelected) return; + _isSelected = value; + OnPropertyChanged(); + } + } + + public double HorizontalOffset { + get => horizontalOffset_; + set { + if (value.Equals(horizontalOffset_)) return; + horizontalOffset_ = value; + OnPropertyChanged(); + } + } + + // make sure _cells are ordered by their index + private void SortCells() { + var isSorted = true; + var prevIdx = -1; + foreach (var cell in _cells) + if (cell.CellIndex >= prevIdx) + prevIdx = cell.CellIndex; + else + isSorted = false; + + if (!isSorted) + _cells = _cells.OrderBy(c => c.CellIndex).ToList(); + } + + private void UpdateCellHeight() { + foreach (var cell in _cells) + FastGridUtil.SetHeight(cell, RowHeight); + } + + internal void UpdateUI() { + // look at what's visible and what's not, + order by index + SortCells(); + + // IMPORTANT: at this time, I assume all items are NOT complex, that is, showing one cell + // is always fast (since at this time, I'm always showing ALL cells, but the cells that are + // not visible, I'm showing them offscreen) + var x = -HorizontalOffset; + foreach (var cell in _cells) { + var offset = cell.IsCellVisible ? x : -100000; + FastGridUtil.SetLeft(cell, offset); + if (cell.IsCellVisible) + x += cell.Width; + } + } + + + internal void SetCellVisible(FastGridViewColumn column, bool isVisible) { + var idx = FastGridUtil.RefIndex(_columns, column); + Debug.Assert(idx >= 0); + _cells[idx].IsCellVisible = isVisible; + } + internal void SetCellWidth(FastGridViewColumn column, double width) { + var idx = FastGridUtil.RefIndex(_columns, column); + Debug.Assert(idx >= 0); + FastGridUtil.SetWidth(_cells[idx], width); + } + internal void SetCellMinWidth(FastGridViewColumn column, double width) { + var idx = FastGridUtil.RefIndex(_columns, column); + Debug.Assert(idx >= 0); + _cells[idx].MinWidth = width; + } + internal void SetCellMaxWidth(FastGridViewColumn column, double width) { + var idx = FastGridUtil.RefIndex(_columns, column); + Debug.Assert(idx >= 0); + _cells[idx].MaxWidth = width; + } + internal void SetCellIndex(FastGridViewColumn column, int index) { + var idx = FastGridUtil.RefIndex(_columns, column); + Debug.Assert(idx >= 0); + _cells[idx].CellIndex = index; + } + + private void BackgroundChanged() + { + _selection.Background = IsSelected ? SelectedBrush : _transparent; + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) { + var view = FastGridUtil.TryGetAscendant(this); + view?.OnMouseLeftButtonDown(this, eventArgs); + } + + + + + private void vm_PropertyChanged(string propertyName) { + switch (propertyName) { + case "RowHeight": + UpdateCellHeight(); + UpdateUI(); + break; + + case "SelectedBrush": + case "IsSelected": + BackgroundChanged(); + break; + + case "HorizontalOffset": + UpdateUI(); + break; + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + if (_loaded) + vm_PropertyChanged(propertyName); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewRowContent.cs b/src/FastControls/FastGrid/FastGridViewRowContent.cs new file mode 100644 index 0000000..f18ceb2 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewRowContent.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewRowContent : ContentControl + { + public FastGridViewRowContent() { + CustomLayout = true; + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + DataContextChanged += FastGridViewRow_DataContextChanged; + } + + private void FastGridViewRow_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { + var childCount = VisualTreeHelper.GetChildrenCount(this); + if (childCount < 1) + return; + var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter; + if (cp == null) + return; + cp.CustomLayout = true; + cp.HorizontalAlignment = HorizontalAlignment.Stretch; + cp.VerticalAlignment = VerticalAlignment.Stretch; + cp.DataContext = DataContext; + } + + public override void OnApplyTemplate() { + base.OnApplyTemplate(); + var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter; + cp.CustomLayout = true; + cp.HorizontalAlignment = HorizontalAlignment.Stretch; + cp.VerticalAlignment = VerticalAlignment.Stretch; + cp.DataContext = DataContext; + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewSort.cs b/src/FastControls/FastGrid/FastGridViewSort.cs new file mode 100644 index 0000000..fe93d22 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewSort.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewSort { + private FastGridView _self; + private SortComparer _sort; + private List _sortedList; + private HashSet _propertyNames = new HashSet(); + + public IReadOnlyList SortedItems => _sortedList ?? _self.FilteredItems; + + private class SortComparer : IComparer { + private FastGridViewSort _self; + private List<(PropertyInfo Property, bool Ascending)> _compareProperties; + + public SortComparer(FastGridViewSort self) { + _self = self; + } + + public void RecomputeProperties() { + _compareProperties = null; + } + + private void RecomputeProperties(object obj) { + _compareProperties = new List<(PropertyInfo,bool)>(); + var type = obj.GetType(); + foreach (var col in _self._self.SortDescriptors.Columns) { + var pi = type.GetProperty(col.Column.DataBindingPropertyName, BindingFlags.Instance | BindingFlags.Public); + if (pi == null) + throw new Exception($"Fastgrid: can't find property {col.Column.DataBindingPropertyName}"); + _compareProperties.Add((pi, col.SortDirection == SortDirection.Ascending)); + } + } + + public int Compare(object a, object b) { + Debug.Assert(a != null && b != null); + if (_compareProperties == null) + RecomputeProperties(a); + + foreach (var prop in _compareProperties) { + var aValue = prop.Property.GetValue(a); + var bValue = prop.Property.GetValue(b); + var compare = CompareValue(aValue, bValue); + if (compare != 0) + return prop.Ascending ? compare : -compare; + } + + return 0; + } + + private int CompareValue(object a, object b) { + if (a is int) + return (int)a - (int)b; + if (a is uint) + return (uint)a < (uint)b ? -1 : ( (uint)a > (uint)b ? 1 : 0 ); + if (a is long) + return (long)a < (long)b ? -1 : ( (long)a > (long)b ? 1 : 0 ); + if (a is short) + return (short)a < (short)b ? -1 : ( (short)a > (short)b ? 1 : 0 ); + if (a is ulong) + return (ulong)a < (ulong)b ? -1 : ( (ulong)a > (ulong)b ? 1 : 0 ); + if (a is ushort) + return (ushort)a < (ushort)b ? -1 : ( (ushort)a > (ushort)b ? 1 : 0 ); + + if (a is byte) + return (byte)a < (byte)b ? -1 : ( (byte)a > (byte)b ? 1 : 0 ); + if (a is char) + return (char)a < (char)b ? -1 : ( (char)a > (char)b ? 1 : 0 ); + + if (a is double) + return (double)a < (double)b ? -1 : ( (double)a > (double)b ? 1 : 0 ); + if (a is float) + return (float)a < (float)b ? -1 : ( (float)a > (float)b ? 1 : 0 ); + if (a is string) { + var aString = (string)a; + var bString = (string)b; + return String.Compare(aString, bString, StringComparison.Ordinal); + } + if (a is DateTime) + return (DateTime)a < (DateTime)b ? -1 : ( (DateTime)a > (DateTime)b ? 1 : 0 ); + + Debug.Assert(false); + throw new Exception($"Type {a.GetType().ToString()} -- don't know how to Compare"); + } + } + + public FastGridViewSort(FastGridView self) { + _self = self; + _sort = new SortComparer(this); + } + + // does a complete resort, ignoring anything we previously cached + public void FullResort() { + if (_sortedList != null) + foreach (var item in _sortedList.OfType()) + item.PropertyChanged -= Item_PropertyChanged; + + var needsSort = _self.SortDescriptors.Count > 0; + if (!needsSort) { + // optimization - we don't have any sort, and no filtering + _sortedList = null; + return; + } + _sortedList = _self.FilteredItems.ToList(); + _propertyNames = new HashSet(_self.SortDescriptors.Columns.Select(c => c.Column.DataBindingPropertyName)); + _sort.RecomputeProperties(); + + foreach (var item in _sortedList.OfType()) + item.PropertyChanged += Item_PropertyChanged; + + _sortedList.Sort(_sort); + } + + private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e) { + if (_propertyNames.Contains(e.PropertyName)) + _self.NeedsResort(); + } + + // the difference between full resort and resort - on the re-sort, the filtered items haven't changed, however, + // at least an object's Sort property has changed + // + // Example: I'm sorting by username, and for an object, I've updated the username + public void Resort() { + _sortedList.Sort(_sort); + } + + public void SortedAdd(object obj) { + if (_sortedList == null) + return; + + var index = _sortedList.BinarySearch(obj, _sort); + if (index < 0) { + _sortedList.Insert(~index, obj); + if (obj is INotifyPropertyChanged npc) + npc.PropertyChanged += Item_PropertyChanged; + } + else + _sortedList[index] = obj; + } + + private int IndexOf(object obj) { + var index = _sortedList.BinarySearch(obj, _sort); + if (index >= 0) { + // note: several object may be equivalent + while (index < _sortedList.Count && !ReferenceEquals(obj, _sortedList[index])) { + ++index; + if (index < _sortedList.Count) + if (_sort.Compare(obj, _sortedList[index]) != 0) + // object not found + break; + } + } + return -1; + } + + public void Remove(object obj) { + if (_sortedList == null) + return; + + var index = IndexOf(obj); + if (index >= 0) { + _sortedList.RemoveAt(index); + + if (obj is INotifyPropertyChanged npc) + npc.PropertyChanged += Item_PropertyChanged; + } + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewSortDescriptors.cs b/src/FastControls/FastGrid/FastGridViewSortDescriptors.cs new file mode 100644 index 0000000..e620a8b --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewSortDescriptors.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FastGrid.FastGrid +{ + public enum SortDirection { + Ascending, Descending, + } + + public class FastGridSortDescriptor { + public FastGridViewColumn Column { get; set; } = null; + public SortDirection SortDirection { get; set; } = SortDirection.Ascending; + } + + public class FastGridViewSortDescriptors { + private List _sortDescriptors = new List(); + public Action OnResort; + + public IReadOnlyList Columns => _sortDescriptors; + public int Count => Columns.Count; + + public void Add(FastGridSortDescriptor sortDescriptor) { + var existingIdx = _sortDescriptors.FindIndex(sd => ReferenceEquals(sd.Column, sortDescriptor.Column)); + if (existingIdx >= 0) + _sortDescriptors[existingIdx].SortDirection = sortDescriptor.SortDirection; + else + _sortDescriptors.Add(sortDescriptor); + OnResort?.Invoke(); + } + + public void Remove(FastGridSortDescriptor sortDescriptor) { + var existingIdx = _sortDescriptors.FindIndex(sd => ReferenceEquals(sd.Column, sortDescriptor.Column)); + if (existingIdx >= 0) + _sortDescriptors.RemoveAt(existingIdx); + OnResort?.Invoke(); + } + + public void Clear() { + foreach (var sort in _sortDescriptors) + sort.Column.Sort = null; + _sortDescriptors.Clear(); + OnResort?.Invoke(); + } + } +} diff --git a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj index 063cc98..07160b5 100644 --- a/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj +++ b/src/FastControls/OpenSilver.ControlsKit.FastControls.csproj @@ -19,4 +19,10 @@ + + + MSBuild:Compile + + + From 895f6b01caba2fdf04a1a8f105e327ee0739d79f Mon Sep 17 00:00:00 2001 From: John Torjo Date: Fri, 3 Feb 2023 10:34:40 +0200 Subject: [PATCH 7/8] latest updates on FastGrid --- .../FastControls.TestApp/MockViewModel.cs | 51 +- .../TestFastGridView.xaml | 20 +- .../TestFastGridView.xaml.cs | 73 +- src/FastControls/Annotations.cs | 1703 +++++++++++++++++ .../FastGrid/FastGridContentTemplate.cs | 82 +- src/FastControls/FastGrid/FastGridView.xaml | 13 +- .../FastGrid/FastGridView.xaml.cs | 298 ++- src/FastControls/FastGrid/FastGridViewCell.cs | 33 +- .../FastGrid/FastGridViewColumn.cs | 31 +- .../FastGridViewColumnCollectionInternal.cs | 19 + .../FastGrid/FastGridViewFilter.cs | 30 - .../FastGrid/FastGridViewFilterItem.cs | 235 --- src/FastControls/FastGrid/FastGridViewRow.cs | 39 +- .../FastGrid/Filter/FastGridViewFilter.cs | 71 + .../Filter/FastGridViewFilterCtrl.xaml | 110 ++ .../Filter/FastGridViewFilterCtrl.xaml.cs | 92 + .../FastGrid/Filter/FastGridViewFilterItem.cs | 427 +++++ .../FastGrid/Filter/FastGridViewFilterUtil.cs | 204 ++ .../Filter/FastGridViewFilterValueItem.cs | 37 + .../Filter/FastGridViewFilterViewModel.cs | 52 + .../Filter/PropertyValueCompareEquivalent.cs | 64 + .../FastGrid/{ => Sort}/FastGridViewSort.cs | 44 +- .../{ => Sort}/FastGridViewSortDescriptors.cs | 0 ...OpenSilver.ControlsKit.FastControls.csproj | 14 +- src/OpenSilver.ControlsKit.sln.DotSettings | 2 + 25 files changed, 3338 insertions(+), 406 deletions(-) create mode 100644 src/FastControls/Annotations.cs delete mode 100644 src/FastControls/FastGrid/FastGridViewFilter.cs delete mode 100644 src/FastControls/FastGrid/FastGridViewFilterItem.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilter.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterCtrl.xaml create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterCtrl.xaml.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterItem.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterUtil.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterValueItem.cs create mode 100644 src/FastControls/FastGrid/Filter/FastGridViewFilterViewModel.cs create mode 100644 src/FastControls/FastGrid/Filter/PropertyValueCompareEquivalent.cs rename src/FastControls/FastGrid/{ => Sort}/FastGridViewSort.cs (76%) rename src/FastControls/FastGrid/{ => Sort}/FastGridViewSortDescriptors.cs (100%) create mode 100644 src/OpenSilver.ControlsKit.sln.DotSettings diff --git a/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs index f9e123a..6c191ca 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs +++ b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs @@ -1,14 +1,15 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Windows.Media; using FastGrid.FastGrid; +using OpenSilver.ControlsKit.Annotations; namespace FastControls.TestApp { - - public class Pullout : INotifyPropertyChanged{ + public class DummyDate : INotifyPropertyChanged{ private int operatorRecordId; private string operatorReportLabel = ""; private string password = ""; @@ -17,6 +18,7 @@ public class Pullout : INotifyPropertyChanged{ private string city; private int vehicleId; private int pulloutId; + private DateTime time_ = DateTime.Now; public int PulloutId { get => pulloutId; @@ -92,6 +94,17 @@ public string City } } + public DateTime Time { + get => time_; + set { + if (value.Equals(time_)) return; + time_ = value; + OnPropertyChanged(); + } + } + + public string TimeString => Time.ToString("HH:mm"); + public Brush BgColor { get { switch (OperatorRecordId % 4) { @@ -119,6 +132,7 @@ public int VehicleId { public event PropertyChangedEventHandler PropertyChanged; + [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } @@ -131,9 +145,11 @@ public class MockViewModel public MockViewModel() { } - public IEnumerable GetPulloutsByCount(int count, int offset = 0) { - for (int i = offset; i < count + offset; ++i) - yield return new Pullout { + public IEnumerable GetPulloutsByCount(int count, int offset = 0) { + var time = DateTime.Now; + var incrementTimeSecs = 60; + for (int i = offset; i < count + offset; ++i) { + yield return new DummyDate { OperatorReportLabel = $"Operator {i}" , OperatorRecordId = i, VehicleId = i , @@ -141,15 +157,34 @@ public IEnumerable GetPulloutsByCount(int count, int offset = 0) { Password = $"Pass {i}", Department = $"Dep {i}", City = $"City {i}", + Time = time, }; + time = time.AddSeconds(incrementTimeSecs); + } } - } - + public IEnumerable GetPulloutsByCountForTestingFilter(int count, int offset = 0) { + var time = DateTime.Now; + var incrementTimeSecs = 10; + for (int i = offset; i < count + offset; ++i) { + yield return new DummyDate { + OperatorReportLabel = $"Operator {i % 250}" , + OperatorRecordId = i % 97, + VehicleId = i , + Username = $"User {i % 29}", + Password = $"Pass {i % 37}", + Department = $"Dep {i % 61}", + City = $"City {i % 23}", + Time = time, + }; + time = time.AddSeconds(incrementTimeSecs); + } + } + } diff --git a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml index 39843f3..39f6c1f 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml @@ -6,17 +6,20 @@ xmlns:conv="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" Loaded="Page_Loaded"> - + - + - + @@ -59,6 +62,17 @@ + + + + + + + + + + + diff --git a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs index 0634684..be7850c 100644 --- a/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs @@ -1,19 +1,22 @@ -using System; +using DotNetForHtml5.Core; +using System; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Data; using System.Windows.Media; using FastControls.TestApp; -using FastGrid.FastGrid; namespace FastGrid.FastGrid { public partial class TestFastGridView : Page { - private ObservableCollection _pullouts; + private ObservableCollection _pullouts; public TestFastGridView() { this.InitializeComponent(); @@ -33,12 +36,12 @@ private async Task TestSimulateScroll() } } - private int RefIndex(Pullout pullout) + private int RefIndex(DummyDate dummyDate) { int idx = 0; _pullouts.FirstOrDefault(i => { - if (ReferenceEquals(i, pullout)) + if (ReferenceEquals(i, dummyDate)) return true; ++idx; return false; @@ -47,7 +50,7 @@ private int RefIndex(Pullout pullout) } private async Task TestSimulateInsertionDeletions() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); ctrl.ItemsSource = _pullouts; await Task.Delay(2000); @@ -81,7 +84,7 @@ private async Task TestSimulateInsertionDeletions() private async Task TestConstantUpdates() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); ctrl.ItemsSource = _pullouts; await Task.Delay(2000); @@ -126,7 +129,7 @@ private async Task TestConstantUpdates() } private async Task TestBoundBackground() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); ctrl.ItemsSource = _pullouts; ctrl.RowTemplate = FastGridContentTemplate.BindBackgroundRowTemplate("BgColor"); await Task.Delay(2000); @@ -140,7 +143,7 @@ private async Task TestBoundBackground() { } private async Task TestAddAndRemoveSorted() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); ctrl.ItemsSource = _pullouts; ctrl.SortDescriptors.Add(new FastGridSortDescriptor { Column = ctrl.Columns["OperatorReportLabel"], SortDirection = SortDirection.Descending}); @@ -157,7 +160,7 @@ private async Task TestAddAndRemoveSorted() { } private async Task TestResortingExistingItems() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5, 50)); ctrl.ItemsSource = _pullouts; ctrl.SortDescriptors.Add(new FastGridSortDescriptor { Column = ctrl.Columns["OperatorReportLabel"], SortDirection = SortDirection.Descending}); @@ -173,7 +176,7 @@ private async Task TestResortingExistingItems() { private async Task TestRowBackgroundFunc() { var reverseEven = false; ctrl.RowBackgroundColorFunc = (o) => { - var pullout = o as Pullout; + var pullout = o as DummyDate; var isEven = pullout.OperatorRecordId % 2 == 0; if (reverseEven) isEven = !isEven; @@ -181,7 +184,7 @@ private async Task TestRowBackgroundFunc() { return BrushCache.Inst.GetByColor(color); }; - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); ctrl.ItemsSource = _pullouts; for (int i = 0; i < 100; ++i) { @@ -192,20 +195,44 @@ private async Task TestRowBackgroundFunc() { } private void SimpleTest() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(5)); ctrl.ItemsSource = _pullouts; ctrl.AllowSortByMultipleColumns = false; ctrl.Columns[1].Sort = true; ctrl.AllowMultipleSelection = true; ctrl.SelectionChanged += (s, a) => { - var sel = Enumerable.OfType(ctrl.GetSelection()); + var sel = ctrl.GetSelection().OfType(); + Console.WriteLine($"new selection {string.Join(",", sel.Select(p => p.Username))}"); + }; + } + // the idea - have special data that will generate diverse data for filtering + private void SimpleTestFilter() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCountForTestingFilter(5000)); + ctrl.ItemsSource = _pullouts; + ctrl.AllowSortByMultipleColumns = false; +// ctrl.Columns[1].Sort = true; + + ctrl.AllowMultipleSelection = true; + ctrl.SelectionChanged += (s, a) => { + var sel = ctrl.GetSelection().OfType(); + Console.WriteLine($"new selection {string.Join(",", sel.Select(p => p.Username))}"); + }; + } + + private void SimpleTestFilterFewItmes() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCountForTestingFilter(5)); + ctrl.ItemsSource = _pullouts; + ctrl.AllowSortByMultipleColumns = false; + + ctrl.SelectionChanged += (s, a) => { + var sel = ctrl.GetSelection().OfType(); Console.WriteLine($"new selection {string.Join(",", sel.Select(p => p.Username))}"); }; } private async Task TestOffscreen() { - _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); ctrl.ItemsSource = _pullouts; for (int i = 0; i < 50; ++i) { await Task.Delay(4000); @@ -213,8 +240,22 @@ private async Task TestOffscreen() { } } + private async Task TestChangeColumnOrder() { + _pullouts = new ObservableCollection(new MockViewModel().GetPulloutsByCount(500)); + ctrl.ItemsSource = _pullouts; + await Task.Delay(3000); + var idx = 0; + foreach (var col in ctrl.Columns) + col.DisplayIndex = (idx++ + 3) % ctrl.Columns.Count; + } + private async void Page_Loaded(object sender, RoutedEventArgs e) { - SimpleTest(); + + ctrl.Columns["Time"].FilterCompareEquivalent.DateTimeFormat = "HH:mm"; + //SimpleTest(); + SimpleTestFilter(); + //SimpleTestFilterFewItmes(); + //await TestChangeColumnOrder(); //await TestOffscreen(); //await TestRowBackgroundFunc(); diff --git a/src/FastControls/Annotations.cs b/src/FastControls/Annotations.cs new file mode 100644 index 0000000..a456b13 --- /dev/null +++ b/src/FastControls/Annotations.cs @@ -0,0 +1,1703 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#nullable disable + +using System; +// ReSharper disable UnusedType.Global + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace OpenSilver.ControlsKit.Annotations +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so checking for null is required before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element can never be null. + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by the format pattern and (optional) arguments. + /// The parameter, which contains the format string, should be given in the constructor. The format string + /// should be in -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; } + } + + /// + /// Indicates that the marked parameter is a message template where placeholders are to be replaced by the following arguments + /// in the order in which they appear + /// + /// + /// void LogInfo([StructuredMessageTemplate]string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// LogInfo("User created: {username}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class StructuredMessageTemplateAttribute : Attribute {} + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It's allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are inclusive. + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] + public sealed class ValueRangeAttribute : Attribute + { + public object From { get; } + public object To { get; } + + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) + { + From = To = value; + } + + public ValueRangeAttribute(ulong value) + { + From = To = value; + } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] + public sealed class NonNegativeValueAttribute : Attribute { } + + /// + /// Indicates that the function argument should be a string literal and match + /// one of the parameters of the caller function. This annotation is used for paramerers + /// like 'string paramName' parameter of the constuctor. + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// Value canbenull is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute + /// with rows separated by the semicolon. There is no notion of order rows, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will be ignored by usage-checking inspections.
+ /// You can use and + /// to configure how this attribute is applied. + ///
+ /// + /// [UsedImplicitly] + /// public class TypeConverter {} + /// + /// public class SummaryData + /// { + /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + /// public SummaryData() {} + /// } + /// + /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Default)] + /// public interface IService {} + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , + /// indicates that the corresponding type is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specifies the details of implicitly used symbol when it is marked + /// with or . + /// + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specifies what is considered to be used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of the type marked with the attribute are considered used. + Members = 2, + /// Inherited entities are considered used. + WithInheritors = 4, + /// Entity marked with the attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API, + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; } + } + + /// + /// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate can only be invoked during method execution + /// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later, + /// when the containing method is no longer on the execution stack). + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// If is true, the attribute will only takes effect if the method invocation is located under the 'await' expression. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute + { + /// + /// Require the method invocation to be used under the 'await' expression for this attribute to take effect on code analysis engine. + /// Can be used for delegate/enumerable parameters of 'async' methods. + /// + public bool RequireAwait { get; set; } + } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help to detect usages of the method when the return value is not used. + /// Optionally, you can specify a message to use when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; } + } + + /// + /// This annotation allows to enforce allocation-less usage patterns of delegates for performance-critical APIs. + /// When this annotation is applied to the parameter of delegate type, IDE checks the input argument of this parameter: + /// * When lambda expression or anonymous method is passed as an argument, IDE verifies that the passed closure + /// has no captures of the containing local variables and the compiler is able to cache the delegate instance + /// to avoid heap allocations. Otherwise the warning is produced. + /// * IDE warns when method name or local function name is passed as an argument as this always results + /// in heap allocation of the delegate instance. + /// + /// + /// In C# 9.0 code IDE would also suggest to annotate the anonymous function with 'static' modifier + /// to make use of the similar analysis provided by the language/compiler. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RequireStaticDelegateAttribute : Attribute + { + public bool IsError { get; set; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute { } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + [CanBeNull] public string Target { get; set; } + } + + /// + /// Indicates how method, constructor invocation, or property access + /// over collection type affects the contents of the collection. + /// When applied to a return value of a method indicates if the returned collection + /// is created exclusively for the caller (CollectionAccessType.UpdatedContent) or + /// can be read/updated from outside (CollectionAccessType.Read | CollectionAccessType.UpdatedContent) + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.ReturnValue)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that the method is a pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that the marked parameter, field, or property is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RegexPatternAttribute : Attribute { } + + /// + /// Language of injected code fragment inside marked by string literal. + /// + public enum InjectedLanguage + { + CSS, + HTML, + JAVASCRIPT, + JSON, + XML + } + + /// + /// Indicates that the marked parameter, field, or property is accepting a string literal + /// containing code fragment in a language specified by the . + /// + /// + /// void Foo([LanguageInjection(InjectedLanguage.CSS, Prefix = "body{", Suffix = "}")] string cssProps) + /// { + /// // cssProps should only contains a list of CSS properties + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class LanguageInjectionAttribute : Attribute + { + public LanguageInjectionAttribute(InjectedLanguage injectedLanguage) + { + InjectedLanguage = injectedLanguage; + } + + /// Specify a language of injected code fragment. + public InjectedLanguage InjectedLanguage { get; } + + /// Specify a string that "precedes" injected string literal. + [CanBeNull] public string Prefix { get; set; } + + /// Specify a string that "follows" injected string literal. + [CanBeNull] public string Suffix { get; set; } + } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class NoReorderAttribute : Attribute { } + + /// + /// + /// Defines the code search template using the Structural Search and Replace syntax. + /// It allows you to find and, if necessary, replace blocks of code that match a specific pattern. + /// Search and replace patterns consist of a textual part and placeholders. + /// Textural part must contain only identifiers allowed in the target language and will be matched exactly (white spaces, tabulation characters, and line breaks are ignored). + /// Placeholders allow matching variable parts of the target code blocks. + /// A placeholder has the following format: $placeholder_name$- where placeholder_name is an arbitrary identifier. + /// + /// + /// Available placeholders: + /// + /// $this$ - expression of containing type + /// $thisType$ - containing type + /// $member$ - current member placeholder + /// $qualifier$ - this placeholder is available in the replace pattern and can be used to insert qualifier expression matched by the $member$ placeholder. + /// (Note that if $qualifier$ placeholder is used, then $member$ placeholder will match only qualified references) + /// $expression$ - expression of any type + /// $identifier$ - identifier placeholder + /// $args$ - any number of arguments + /// $arg$ - single argument + /// $arg1$ ... $arg10$ - single argument + /// $stmts$ - any number of statements + /// $stmt$ - single statement + /// $stmt1$ ... $stmt10$ - single statement + /// $name{Expression, 'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $expression{'Namespace.FooType'}$ - expression with 'Namespace.FooType' type + /// $name{Type, 'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $type{'Namespace.FooType'}$ - 'Namespace.FooType' type + /// $statement{1,2}$ - 1 or 2 statements + /// + /// + /// + /// Note that you can also define your own placeholders of the supported types and specify arguments for each placeholder type. + /// This can be done using the following format: $name{type, arguments}$. Where 'name' - is the name of your placeholder, + /// 'type' - is the type of your placeholder (one of the following: Expression, Type, Identifier, Statement, Argument, Member), + /// 'arguments' - arguments list for your placeholder. Each placeholder type supports it's own arguments, check examples below for mode details. + /// Placeholder type may be omitted and determined from the placeholder name, if name has one of the following prefixes: + /// + /// expr, expression - expression placeholder, e.g. $exprPlaceholder{}$, $expressionFoo{}$ + /// arg, argument - argument placeholder, e.g. $argPlaceholder{}$, $argumentFoo{}$ + /// ident, identifier - identifier placeholder, e.g. $identPlaceholder{}$, $identifierFoo{}$ + /// stmt, statement - statement placeholder, e.g. $stmtPlaceholder{}$, $statementFoo{}$ + /// type - type placeholder, e.g. $typePlaceholder{}$, $typeFoo{}$ + /// member - member placeholder, e.g. $memberPlaceholder{}$, $memberFoo{}$ + /// + /// + /// + /// Expression placeholder arguments: + /// + /// expressionType - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myExpr{Expression, 'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// $myExpr{Expression, 'Namespace.FooType'}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type or expressions which can be implicitly converted to 'Namespace.FooType'. + /// $myExpr{Expression}$ - defines expression placeholder, matching expressions of any type. + /// $exprFoo{'Namespace.FooType', true}$ - defines expression placeholder, matching expressions of the 'Namespace.FooType' type with exact matching. + /// + /// + /// + /// Type placeholder arguments: + /// + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myType{Type, 'Namespace.FooType', true}$ - defines type placeholder, matching 'Namespace.FooType' types with exact matching. + /// $myType{Type, 'Namespace.FooType'}$ - defines type placeholder, matching 'Namespace.FooType' types or types, which can be implicitly converted to 'Namespace.FooType'. + /// $myType{Type}$ - defines type placeholder, matching any type. + /// $typeFoo{'Namespace.FooType', true}$ - defines types placeholder, matching 'Namespace.FooType' types with exact matching. + /// + /// + /// + /// Identifier placeholder arguments: + /// + /// nameRegex - string value in single quotes, specifies regex to use for matching (empty string by default) + /// nameRegexCaseSensitive - boolean value, specifies if name regex is case sensitive (true by default) + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myIdentifier{Identifier, 'my.*', false, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (ignoring case) starting with 'my' prefix with 'Namespace.FooType' type. + /// $myIdentifier{Identifier, 'my.*', true, 'Namespace.FooType', true}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix with 'Namespace.FooType' type. + /// $identFoo{'my.*'}$ - defines identifier placeholder, matching identifiers (case sensitively) starting with 'my' prefix. + /// + /// + /// + /// Statement placeholder arguments: + /// + /// minimalOccurrences - minimal number of statements to match (-1 by default) + /// maximalOccurrences - maximal number of statements to match (-1 by default) + /// + /// Examples: + /// + /// $myStmt{Statement, 1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// $myStmt{Statement}$ - defines statement placeholder, matching any number of statements. + /// $stmtFoo{1, 2}$ - defines statement placeholder, matching 1 or 2 statements. + /// + /// + /// + /// Argument placeholder arguments: + /// + /// minimalOccurrences - minimal number of arguments to match (-1 by default) + /// maximalOccurrences - maximal number of arguments to match (-1 by default) + /// + /// Examples: + /// + /// $myArg{Argument, 1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// $myArg{Argument}$ - defines argument placeholder, matching any number of arguments. + /// $argFoo{1, 2}$ - defines argument placeholder, matching 1 or 2 arguments. + /// + /// + /// + /// Member placeholder arguments: + /// + /// docId - string value in single quotes, specifies XML documentation id of the member to match (empty by default) + /// + /// Examples: + /// + /// $myMember{Member, 'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// $memberFoo{'M:System.String.IsNullOrEmpty(System.String)'}$ - defines member placeholder, matching 'IsNullOrEmpty' member of the 'System.String' type. + /// + /// + /// + /// For more information please refer to the Structural Search and Replace article. + /// + /// + [AttributeUsage( + AttributeTargets.Method + | AttributeTargets.Constructor + | AttributeTargets.Property + | AttributeTargets.Field + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum, + AllowMultiple = true, + Inherited = false)] + public sealed class CodeTemplateAttribute : Attribute + { + public CodeTemplateAttribute(string searchTemplate) + { + SearchTemplate = searchTemplate; + } + + /// + /// Structural search pattern to use in the code template. + /// Pattern includes textual part, which must contain only identifiers allowed in the target language, + /// and placeholders, which allow matching variable parts of the target code blocks. + /// + public string SearchTemplate { get; } + + /// + /// Message to show when the search pattern was found. + /// You can also prepend the message text with "Error:", "Warning:", "Suggestion:" or "Hint:" prefix to specify the pattern severity. + /// Code patterns with replace template produce suggestions by default. + /// However, if replace template is not provided, then warning severity will be used. + /// + public string Message { get; set; } + + /// + /// Structural search replace pattern to use in code template replacement. + /// + public string ReplaceTemplate { get; set; } + + /// + /// Replace message to show in the light bulb. + /// + public string ReplaceMessage { get; set; } + + /// + /// Apply code formatting after code replacement. + /// + public bool FormatAfterReplace { get; set; } = true; + + /// + /// Whether similar code blocks should be matched. + /// + public bool MatchSimilarConstructs { get; set; } + + /// + /// Automatically insert namespace import directives or remove qualifiers that become redundant after the template is applied. + /// + public bool ShortenReferences { get; set; } + + /// + /// String to use as a suppression key. + /// By default the following suppression key is used 'CodeTemplate_SomeType_SomeMember', + /// where 'SomeType' and 'SomeMember' are names of the associated containing type and member to which this attribute is applied. + /// + public string SuppressionKey { get; set; } + } + + #region ASP.NET + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; } + + [NotNull] public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + #endregion + + #region ASP.NET MVC + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute { } + + #endregion + + #region ASP.NET Routing + + /// + /// Indicates that the marked parameter, field, or property is a route template. + /// + /// + /// This attribute allows IDE to recognize the use of web frameworks' route templates + /// to enable syntax highlighting, code completion, navigation, rename and other features in string literals. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RouteTemplateAttribute : Attribute { } + + /// + /// Indicates that the marked type is custom route parameter constraint, + /// which is registered in application's Startup with name ConstraintName + /// + /// + /// You can specify ProposedType if target constraint matches only route parameters of specific type, + /// it will allow IDE to create method's parameter from usage in route template + /// with specified type instead of default System.String + /// and check if constraint's proposed type conflicts with matched parameter's type + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class RouteParameterConstraintAttribute : Attribute + { + [NotNull] public string ConstraintName { get; } + [CanBeNull] public Type ProposedType { get; set; } + + public RouteParameterConstraintAttribute([NotNull] string constraintName) + { + ConstraintName = constraintName; + } + } + + /// + /// Indicates that the marked parameter, field, or property is an URI string. + /// + /// + /// This attribute enables code completion, navigation, rename and other features + /// in URI string literals assigned to annotated parameter, field or property. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class UriStringAttribute : Attribute + { + public UriStringAttribute() { } + + public UriStringAttribute(string httpVerb) + { + HttpVerb = httpVerb; + } + + [CanBeNull] public string HttpVerb { get; } + } + + /// + /// Indicates that the marked method declares routing convention for ASP.NET + /// + /// + /// IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AspRouteConventionAttribute : Attribute + { + public AspRouteConventionAttribute() { } + + public AspRouteConventionAttribute(string predefinedPattern) + { + PredefinedPattern = predefinedPattern; + } + + [CanBeNull] public string PredefinedPattern { get; } + } + + /// + /// Indicates that the marked method parameter contains default route values of routing convention for ASP.NET + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspDefaultRouteValuesAttribute : Attribute { } + + /// + /// Indicates that the marked method parameter contains constraints on route values of routing convention for ASP.NET + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspRouteValuesConstraintsAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains routing order provided by ASP.NET routing attribute + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] + public sealed class AspRouteOrderAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains HTTP verbs provided by ASP.NET routing attribute + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] + public sealed class AspRouteVerbsAttribute : Attribute { } + + /// + /// Indicates that the marked attribute is used for attribute routing in ASP.NET + /// + /// + /// IDE will analyze all usages of attributes marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class AspAttributeRoutingAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked method declares ASP.NET Minimal API endpoint + /// + /// + /// IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AspMinimalApiDeclarationAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked parameter contains ASP.NET Minimal API endpoint handler + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMinimalApiHandlerAttribute : Attribute { } + + #endregion + + #region Razor + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; } + + [NotNull] public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute([NotNull] string baseType) + { + BaseType = baseType; + } + public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] public string BaseType { get; } + [CanBeNull] public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute { } + + #endregion + + #region XAML + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some Style-derived type, that + /// is used to style items of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemStyleOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has OneWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlOneWayBindingModeByDefaultAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has TwoWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, to DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class XamlTwoWayBindingModeByDefaultAttribute : Attribute { } + + #endregion +} \ No newline at end of file diff --git a/src/FastControls/FastGrid/FastGridContentTemplate.cs b/src/FastControls/FastGrid/FastGridContentTemplate.cs index 643fa70..c4c7a23 100644 --- a/src/FastControls/FastGrid/FastGridContentTemplate.cs +++ b/src/FastControls/FastGrid/FastGridContentTemplate.cs @@ -8,6 +8,7 @@ using System.Windows.Data; using System.Windows.Input; using System.Windows.Shapes; +using DotNetForHtml5.Core; namespace FastGrid.FastGrid { @@ -42,7 +43,7 @@ public static DataTemplate DefaultRowTemplate() { return dt; } - private static Geometry SortPath() { + private static Geometry SortGeometry() { var pf = new PathFigure { StartPoint = new Point(3,0), Segments = new PathSegmentCollection(new PathSegment[] { @@ -53,7 +54,47 @@ private static Geometry SortPath() { var g = new PathGeometry(new PathFigure[] { pf }); return g; } + + public static Path SortPath() { + var path = new Path { + Data = SortGeometry(), + Fill = new SolidColorBrush(Colors.Gray), + RenderTransformOrigin = new Point(0.5, 0.5), + Opacity = 0, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 6, 0, 0), + Stretch = Stretch.Fill, + Width = 6, Height = 4, + RenderTransform = new RotateTransform { Angle = 180 }, + }; + return path; + } + + public static Grid FilterPath() { + var geometry = TypeFromStringConverters.ConvertFromInvariantString(typeof(Geometry), + "M0.93340254,0 L4.934082,0 L6.934082,0 L10.93358,0 C11.996876,0 12.199773,0.75 11.668063,1.359375 L8.4335356,5.5 C8.100522,5.8975558 7.983531,6.062263 7.9429321,6.2736206 L7.934082,6.3298788 L7.934082,10.332101 C7.934082,10.332101 3.9340818,14.997499 3.9340818,14.997499 L3.9340818,6.3293748 L3.9286206,6.3012671 C3.8825667,6.1045012 3.751812,5.9296875 3.3865709,5.5 L0.24589038,1.40625 C-0.2067349,0.84375 -0.066181421,1.2241071E-16 0.93340254,0 z") as Geometry; + var path = new Path { + Data = geometry, + //Fill = new SolidColorBrush(Colors.Gray), + Margin = new Thickness(0), + Stretch = Stretch.Fill, + Width = 8, Height = 12, + }; + path.SetBinding(Path.FillProperty, new Binding("Filter.Color")); + var grid = new Grid { + Width = 8, + Background = new SolidColorBrush(Colors.Transparent), + Cursor = Cursors.Hand, + }; + grid.Children.Add(path); + + return grid; + } public static DataTemplate DefaultHeaderTemplate() { + return DefaultHeaderTemplate(new Thickness(5, 0, 5, 0)); + } + public static DataTemplate DefaultHeaderTemplate(Thickness headerMargin) { var dt = FastGridUtil.CreateDataTemplate(() => { const double MIN_WIDTH = 20; /* @@ -62,12 +103,12 @@ public static DataTemplate DefaultHeaderTemplate() { */ var grid = new Grid { - Background = new SolidColorBrush(Colors.Transparent) + Background = new SolidColorBrush(Colors.Transparent), }; var tb = new TextBlock { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(10, 0, 10, 0), + Margin = headerMargin, FontSize = 14, }; grid.SetBinding(Grid.WidthProperty, new Binding("Width")); @@ -77,17 +118,14 @@ public static DataTemplate DefaultHeaderTemplate() { tb.SetBinding(TextBlock.TextProperty, new Binding("HeaderText")); - var path = new Path { - Data = SortPath(), - Fill = new SolidColorBrush(Colors.Gray), - RenderTransformOrigin = new Point(0.5, 0.5), - Opacity = 0, + var path = SortPath(); + var filterButton = new ContentControl { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(10, 0, 10, 0), - Stretch = Stretch.Fill, - Width = 6, Height = 4, - RenderTransform = new RotateTransform { Angle = 180 }, + Margin = new Thickness(0, 0, 10, 0), + Padding = new Thickness(2), + Content = FilterPath(), + Cursor = Cursors.Hand, }; var canvas = new Canvas { @@ -107,6 +145,7 @@ public static DataTemplate DefaultHeaderTemplate() { canvas.Children.Add(transparentRect); grid.Children.Add(tb); grid.Children.Add(path); + grid.Children.Add(filterButton); grid.Children.Add(canvas); grid.DataContextChanged += (s, a) => { var column = (s as FrameworkElement).DataContext as FastGridViewColumn; @@ -138,6 +177,14 @@ public static DataTemplate DefaultHeaderTemplate() { Canvas.SetLeft(transparentRect, a.NewSize.Width - TRANSPARENT_WIDTH / 2); }; + grid.Loaded += (s, a) => { + var column = ((s as FrameworkElement).DataContext as FastGridViewColumn); + if (column.DataBindingPropertyName == "" || !column.IsFilterable) + filterButton.Visibility = Visibility.Collapsed; + if (!column.IsSortable) + path.Visibility = Visibility.Collapsed; + }; + bool mouseDown = false; Point initialPos = new Point(0,0); double initialWidth = 0; @@ -168,6 +215,17 @@ public static DataTemplate DefaultHeaderTemplate() { } a.Handled = true; }; + + filterButton.MouseLeftButtonUp += (s, a) => { + var self = s as FrameworkElement; + var view = FastGridUtil.TryGetAscendant(self); + if (view != null) { + var column = ((s as FrameworkElement).DataContext as FastGridViewColumn); + view.EditFilterMousePos = a.GetPosition(view); + column.IsEditingFilter = !column.IsEditingFilter; + } + a.Handled = true; + }; return grid; }); return dt; diff --git a/src/FastControls/FastGrid/FastGridView.xaml b/src/FastControls/FastGrid/FastGridView.xaml index 69ddbd6..fc89c88 100644 --- a/src/FastControls/FastGrid/FastGridView.xaml +++ b/src/FastControls/FastGrid/FastGridView.xaml @@ -3,10 +3,15 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:FastGrid.FastGrid" + xmlns:conv="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" SizeChanged="UserControl_SizeChanged" Loaded="UserControl_Loaded" Unloaded="UserControl_Unloaded" > - + + + + + @@ -17,6 +22,12 @@ + + + + diff --git a/src/FastControls/FastGrid/FastGridView.xaml.cs b/src/FastControls/FastGrid/FastGridView.xaml.cs index ef9fa76..3052af8 100644 --- a/src/FastControls/FastGrid/FastGridView.xaml.cs +++ b/src/FastControls/FastGrid/FastGridView.xaml.cs @@ -15,21 +15,13 @@ using System.Windows.Media; using System.Windows.Shapes; using System.Windows.Threading; +using FastGrid.FastGrid.Filter; using OpenSilver; +using OpenSilver.ControlsKit.Annotations; namespace FastGrid.FastGrid { /* - - * filtering - * - basically monitor for changes (based on sort, etc.) - * - * + text wrapping - * - * - * - * - * * * IMPORTANT: * Filtering @@ -58,7 +50,13 @@ public partial class FastGridView : UserControl, INotifyPropertyChanged { // these are the rows - not all of them need to be visible, since we'll always make sure we have enough rows to accomodate the whole height of the control private List _rows = new List(); + private IReadOnlyList _items; + private IReadOnlyList _filteredItems; + // this is non-null ONLY when editing a filter + // the idea: when I'm editing a filter -> I will compute how many unique values I have available, based on the OTHER filters + private IReadOnlyList _temporaryFilteredItems = null; + private bool _suspendRender = false; // this is the first visible row - based on this, I re-compute the _topRowIndex, on each update of the UI @@ -90,6 +88,11 @@ public partial class FastGridView : UserControl, INotifyPropertyChanged { private int _successfullyDrawnTopRowIdx = -1;//_topRowIndexWhenNotScrolling private bool _needsRefilter = false, _needsFullReSort = false, _needsReSort = false; + private bool _needsRebuildHeader = false; + + public enum RightClickAutoSelectType { + None, Select, SelectAdd, + } public FastGridView() { this.InitializeComponent(); @@ -151,9 +154,26 @@ public FastGridView() { } private int TopRowIndex => _scrollingTopRowIndex >= 0 ? _scrollingTopRowIndex : _topRowIndexWhenNotScrolling; - private bool IsEmpty => SortedItems == null || SortedItems.Count < 1; - internal IReadOnlyList FilteredItems => _items; + private bool IsEmpty => _items == null || _items.Count < 1; + + internal IReadOnlyList FilteredItems { + get { + if (IsEditingFilter) + return _temporaryFilteredItems ?? _items; + else + return _filteredItems ?? _items; + } + } internal IReadOnlyList SortedItems => _sort.SortedItems; + internal bool IsEditingFilter => Columns.Any(col => col.IsEditingFilter); + internal FastGridViewFilterItem EditFilterItem { + get { + var column = editFilterCtrl.ViewModel.EditColumn; + Debug.Assert(column != null); + var filterItem = Filter.GetOrAddFilterForProperty(column); + return filterItem; + } + } public FastGridViewColumnCollection Columns => _columns; public FastGridViewSortDescriptors SortDescriptors => _sortDescriptors; @@ -191,6 +211,8 @@ public FastGridView() { // if false -> we dim the cells and then do the resize once the user finishes (much faster) public bool InstantColumnResize { get; set; } = false; + internal FastGridViewFilter Filter { get; } = new FastGridViewFilter(); + // optimization: you can set this to true when we're offscreen -- in this case, we'll unbind all rows (so that no unnecessary updates take place) // // by default, if Parent is Canvas, I will check every 0.5 seconds, and if I can determine we're offscreen, I will automatically set this to true @@ -242,6 +264,8 @@ public int UiTimerInterval { // note: not bindable at this time public bool IsFilteringAllowed { get; set; } = false; + internal Point EditFilterMousePos { get; set; } = new Point(); + public IEnumerable VisibleRows() => _rows.Where(r => r.IsRowVisible).Select(r => r.RowObject); public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(FastGridView), @@ -268,7 +292,7 @@ public double RowHeight { } public static readonly DependencyProperty HeaderHeightProperty = DependencyProperty.Register( - "HeaderHeight", typeof(double), typeof(FastGridView), new PropertyMetadata(30d, HeaderHeightChanged)); + "HeaderHeight", typeof(double), typeof(FastGridView), new PropertyMetadata(36d, HeaderHeightChanged)); public double HeaderHeight { get { return (double)GetValue(HeaderHeightProperty); } @@ -366,6 +390,24 @@ public DataTemplate HeaderTemplate { set { SetValue(HeaderTemplateProperty, value); } } + public static readonly DependencyProperty RightClickAutoSelectProperty = DependencyProperty.Register( + "RightClickAutoSelect", typeof(RightClickAutoSelectType), typeof(FastGridView), new PropertyMetadata(RightClickAutoSelectType.None)); + + public RightClickAutoSelectType RightClickAutoSelect { + get { return (RightClickAutoSelectType)GetValue(RightClickAutoSelectProperty); } + set { SetValue(RightClickAutoSelectProperty, value); } + } + + public object RightClickSelectedObject { + get => rightClickSelectedObject_; + private set { + if (Equals(value, rightClickSelectedObject_)) return; + rightClickSelectedObject_ = value; + OnPropertyChanged(); + } + } + + private static void SingleSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { (d as FastGridView).SingleSelectedIndexChanged(); @@ -590,6 +632,19 @@ private int ObjectToRowIndex(object obj, int suggestedFindIndex) return -1; } + // extra optimization - if this is towards the end, and we could actually see more object, return an index that will show as many objects as possbible + // + // example: we set the top row index to zero, then reverse the sorting order -- in this case, our Top object would become the last + // without this optimization, we'd end up seeing only one object + private int ObjectTo_Top_RowIndex(object obj, int suggestedFindIndex) { + var foundIdx = ObjectToRowIndex(obj, suggestedFindIndex); + var maxRowIdx = MaxRowIdx(); + if (foundIdx > maxRowIdx) + return maxRowIdx; + else + return foundIdx; + } + private void ComputeTopRowIndex() { if (SortedItems == null || SortedItems.Count < 1) { @@ -599,7 +654,7 @@ private void ComputeTopRowIndex() return; } - var foundIdx = ObjectToRowIndex(_topRow, _topRowIndexWhenNotScrolling); + var foundIdx = ObjectTo_Top_RowIndex(_topRow, _topRowIndexWhenNotScrolling); if (foundIdx == _topRowIndexWhenNotScrolling) return; // same @@ -620,7 +675,7 @@ private bool CanDraw() { return false; // we're hidden if (_suspendRender) return false; - if (SortedItems == null) + if (_items == null) return false; if (RowHeight < 1) return false; @@ -686,8 +741,16 @@ private bool TryUpdateUI() { _isUpdatingUI = true; try { - if (_needsRefilter) + if (_needsRebuildHeader) { + _needsRebuildHeader = false; + RebuildHeaderCollection(); + } + + if (_needsRefilter) { + _needsRefilter = false; FullReFilter(); + } + if (_needsFullReSort) { _needsFullReSort = false; _needsReSort = false; @@ -695,7 +758,7 @@ private bool TryUpdateUI() { } if (_needsReSort) { _needsReSort = false; - _sort.Resort(); + _sort.FastResort(); } ComputeTopRowIndex(); @@ -1030,11 +1093,21 @@ private FastGridViewRow CreateRow() { }; _rows.Add(row); canvas.Children.Add(row); + row.MouseRightButtonDown += Row_MouseRightButtonDown; Console.WriteLine($"row created({Name}), rows={_rows.Count}"); return row; } + private void Row_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { + RightClickSelectedObject = (sender as FastGridViewRow)?.RowObject; + OnRightClick(sender as FastGridViewRow); + } + private void FastGridView_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + if (IsEditingFilter) + // the idea -- once the user stops filtering, we'll do a full refilter + resort anyway + return; + if (CanUserSortColumns || IsFilteringAllowed) { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -1053,6 +1126,7 @@ private void FastGridView_CollectionChanged(object sender, NotifyCollectionChang _needsRefilter = true; if (CanUserSortColumns) _needsFullReSort = true; + Console.WriteLine($"Fastgrid {Name} - needs refilter/resort"); break; } } @@ -1063,9 +1137,7 @@ private void FastGridView_CollectionChanged(object sender, NotifyCollectionChang private bool MatchesFilter(object item) { if (!IsFilteringAllowed) return true; // no filtering - - // FIXME - return true; + return Filter.Matches(item); } private void OnAddedItem(object item) { @@ -1104,7 +1176,20 @@ private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) { private void CreateHeader() { headerCtrl.ItemTemplate = HeaderTemplate; - headerCtrl.ItemsSource = _columns; + RebuildHeaderCollection(); + } + + private void RebuildHeaderCollection() { + for (var index = 0; index < Columns.Count; index++) { + var column = Columns[index]; + if (column.DisplayIndex < 0) + column.DisplayIndex = index; + } + // once column order changes -> make sure we reflect that instantly + foreach (var row in _rows) + row.UpdateUI(); + + headerCtrl.ItemsSource = Columns.OrderBy(c => c.DisplayIndex).ToList(); } public override void OnApplyTemplate() @@ -1176,6 +1261,7 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e) { _checkOffscreenUiTimer.Start(); HandleFilterSortColumns(); + CreateFilter(); CreateHeader(); HandleContextMenu(); @@ -1184,6 +1270,22 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e) { PostponeUpdateUI(); } + private void CreateFilter() { + if (!CanUserSortColumns && !IsFilteringAllowed) + return; + + // rationale: allow the user to customize the filter for a column + // + // usually, I want to allow customizing the Filter equivalence, for instance, on a Date/time column, I can specify the date/time format + // By default, that's "yyyy/MM/dd HH:mm:ss", but I may want to specify "HH:mm" (in this case, when user would filter by that column, + // he'd see the unique values formatted as HH:mm) + foreach (var col in Columns) + if (col.IsFilterable) { + col.Filter.PropertyName = col.DataBindingPropertyName; + Filter.AddFilter(col.Filter); + } + } + private void HandleFilterSortColumns() { if (!CanUserSortColumns && !IsFilteringAllowed) return; @@ -1200,8 +1302,13 @@ private void HandleFilterSortColumns() { // after this, you need to re-sort private void FullReFilter() { _needsRefilter = false; - // FIXME to implement - // also - if no filter, reuse _items + if (!Filter.IsEmpty) { + var watch = Stopwatch.StartNew(); + _filteredItems = _items.Where(i => Filter.Matches(i)).ToList(); + Console.WriteLine($"Fastgrid {Name} - refilter complete, took {watch.ElapsedMilliseconds} ms"); + } else + // no filter + _filteredItems = null; _needsFullReSort = true; } @@ -1238,12 +1345,11 @@ internal void OnColumnsCollectionChanged() { private double horizontalOffset_ = 0; private bool isScrollingHorizontally_ = false; private bool isOffscreen_ = false; + private object rightClickSelectedObject_ = null; internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyName) { switch (propertyName) { case "IsVisible": - foreach (var row in _rows) - row.SetCellVisible(col, col.IsVisible); PostponeUpdateUI(); break; @@ -1253,9 +1359,11 @@ internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyNam SetRowOpacity(0.4); } else { if (!InstantColumnResize) { - SetRowOpacity(1); + // first, make sure all cells have the correct size, after row resize foreach (var row in _rows) - row.SetCellWidth(col, col.Width); + row.UpdateUI(); + + SetRowOpacity(1); PostponeUpdateUI(); } @@ -1269,26 +1377,21 @@ internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyNam var updateCellWidthNow = InstantColumnResize || !col.IsResizingColumn; if (updateCellWidthNow) { foreach (var row in _rows) - row.SetCellWidth(col, col.Width); + row.UpdateUI(); PostponeUpdateUI(); } break; case "MinWidth": - foreach (var row in _rows) - row.SetCellMinWidth(col, col.MinWidth); PostponeUpdateUI(); break; case "MaxWidth": - foreach (var row in _rows) - row.SetCellMaxWidth(col, col.MaxWidth); PostponeUpdateUI(); break; - case "ColumnIndex": - foreach (var row in _rows) - row.SetCellIndex(col, col.ColumnIndex); + case "DisplayIndex": + _needsRebuildHeader = true; PostponeUpdateUI(); break; @@ -1312,6 +1415,18 @@ internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyNam } break; + case "IsEditingFilter": + if (col.IsEditingFilter) { + foreach (var other in Columns.Where(c => !ReferenceEquals(c, col))) + other.IsEditingFilter = false; + OpenEditFilter(col, EditFilterMousePos); + } else { + if (Columns.All(c => !c.IsEditingFilter)) + CloseEditFilter(col); + } + + break; + case "HeaderText": break; case "CanResize": break; case "CellTemplate": break; @@ -1341,6 +1456,21 @@ private void OnOffscreenChange() { } } + private void OnRightClick(FastGridViewRow row) { + switch (RightClickAutoSelect) { + case RightClickAutoSelectType.None: + break; + case RightClickAutoSelectType.Select: + SelectSet(row); + break; + case RightClickAutoSelectType.SelectAdd: + SelectAdd(row); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + private void vm_PropertyChanged(string name) { switch (name) { case "HorizontalOffset": @@ -1357,9 +1487,107 @@ private void vm_PropertyChanged(string name) { } } + private void filter_ValueItem_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + UpdateEditFilterValues(); + _needsReSort = true; + PostponeUpdateUI(); + } + private void UpdateEditFilterValues() { + var column = editFilterCtrl.ViewModel.EditColumn; + var filterItem = Filter.GetOrAddFilterForProperty(column); + var filterValueItems = editFilterCtrl.ViewModel.FilterValueItems; + // if everything is selected -> no filter + var selectedItems = filterValueItems.All(vi => vi.IsSelected) ? new List<(string AsString, object OriginalValue)>() : filterValueItems.Where(vi => vi.IsSelected).Select(vi => (vi.Text,vi.OriginalValue)).ToList(); + filterItem.PropertyValues = selectedItems; + } + + private void CloseEditFilter(FastGridViewColumn column) { + editFilterCtrl.ViewModel.EditColumn = null; + editFilterPopup.IsOpen = false; + editFilterCtrl.ViewModel.FilterItem.PropertyChanged -= FilterItem_PropertyChanged; + editFilterCtrl.ViewModel.FilterItem = null; + + _temporaryFilteredItems = null; + _needsRefilter = true; + PostponeUpdateUI(); + } + + private void FilterItem_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) { + case "ForceRefreshFilter": + _needsRefilter = true; + PostponeUpdateUI(); + break; + } + } + + private void ComputeTemporaryFilterItems(FastGridViewColumn column) { + Debug.Assert(IsEditingFilter); + + var TempFilter = Filter.Copy(); + TempFilter.RemoveFilter(column.DataBindingPropertyName); + + if (!TempFilter.IsEmpty) + _temporaryFilteredItems = _items.Where(i => TempFilter.Matches(i)).ToList(); + else + // optimization - in this case, there's a single filter + _temporaryFilteredItems = null; + + // resort, based on temporary filtered items + _needsFullReSort = true; + PostponeUpdateUI(); + } + + private void OpenEditFilter(FastGridViewColumn column, Point mouse) { + if (editFilterCtrl.ViewModel.FilterItem != null) + editFilterCtrl.ViewModel.FilterItem.PropertyChanged -= FilterItem_PropertyChanged; + + ComputeTemporaryFilterItems(column); + editFilterCtrl.ViewModel.EditColumn = column; + var filterItem = Filter.GetOrAddFilterForProperty(column); + var uniqueValues = FastGridViewFilterUtil.ToUniqueValues(FilteredItems, column.DataBindingPropertyName, filterItem.CompareEquivalent); + editFilterCtrl.ViewModel.FilterItem = filterItem; + var selectedValues = new HashSet(filterItem.PropertyValues.Select(v => v.AsString)); + var list = uniqueValues.Select(v => new FastGridViewFilterValueItem { + Text = v.AsString, + OriginalValue = v.OriginalValue, + IsSelected = selectedValues.Contains(v.AsString) + }).ToList(); + editFilterCtrl.ViewModel.FilterValueItems = list; + foreach (var item in list) + item.PropertyChanged += filter_ValueItem_PropertyChanged; + + const double PAD = 20; + var x = mouse.X + PAD + editFilterCtrl.Width > canvas.Width ? canvas.Width - editFilterCtrl.Width : mouse.X + PAD; + var y = mouse.Y + PAD; + + if (y + editFilterCtrl.Height > canvas.Height) { + // in this case, if we're shown at the bottom, it's possible that the filter is not fully visible + y = mouse.Y; + var point = canvas.TransformToVisual(null).TransformPoint(new Point(0, 0)); + var maxUp = point.Y; + var goUp = -(canvas.Height - editFilterCtrl.Height - PAD) + PAD; + if (goUp > maxUp) + goUp = maxUp; // go only as much as possible + y -= goUp; + } + + editFilterCtrl.grid.Background = this.Background; + editFilterPopup.HorizontalOffset = x; + editFilterPopup.VerticalOffset = y; + editFilterPopup.IsOpen = true; + editFilterCtrl.Visibility = Visibility.Visible; + + // monitor for manual filter changes + editFilterCtrl.ViewModel.FilterItem.PropertyChanged += FilterItem_PropertyChanged; + } + public event PropertyChangedEventHandler PropertyChanged; + [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { vm_PropertyChanged(propertyName); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/src/FastControls/FastGrid/FastGridViewCell.cs b/src/FastControls/FastGrid/FastGridViewCell.cs index 48d0746..6f50a41 100644 --- a/src/FastControls/FastGrid/FastGridViewCell.cs +++ b/src/FastControls/FastGrid/FastGridViewCell.cs @@ -4,34 +4,22 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using OpenSilver.ControlsKit.Annotations; namespace FastGrid.FastGrid { internal class FastGridViewCell : ContentControl, INotifyPropertyChanged { - private int cellIndex_ = 0; private bool isCellVisible_ = true; - // just move it offscreen - public bool IsCellVisible { - get => isCellVisible_; - set { - if (value == isCellVisible_) return; - isCellVisible_ = value; - OnPropertyChanged(); - } - } + // the reason for this - much easier to resort, when the column's display index changes + private FastGridViewColumn column_; - public int CellIndex { - get => cellIndex_; - set { - if (value == cellIndex_) return; - cellIndex_ = value; - OnPropertyChanged(); - } - } + public bool IsCellVisible => column_.IsVisible; + public int CellIndex => column_.DisplayIndex; - public FastGridViewCell() { + public FastGridViewCell(FastGridViewColumn column) { + column_ = column; CustomLayout = true; HorizontalAlignment = HorizontalAlignment.Stretch; VerticalAlignment = VerticalAlignment.Stretch; @@ -66,8 +54,15 @@ protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) { } + + public void UpdateWidth() { + FastGridUtil.SetWidth(this, column_.Width); + // note: I don't really care about Min/MaxWidth -- the column (header) itself deals with that + } + public event PropertyChangedEventHandler PropertyChanged; + [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/FastControls/FastGrid/FastGridViewColumn.cs b/src/FastControls/FastGrid/FastGridViewColumn.cs index c95bafa..eb67e62 100644 --- a/src/FastControls/FastGrid/FastGridViewColumn.cs +++ b/src/FastControls/FastGrid/FastGridViewColumn.cs @@ -3,6 +3,8 @@ using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; +using FastGrid.FastGrid.Filter; +using OpenSilver.ControlsKit.Annotations; namespace FastGrid.FastGrid { @@ -69,12 +71,12 @@ public string UniqueName { // future: // this is how we're ordering the columns -> at this time, doesn't fully work - public static readonly DependencyProperty ColumnIndexProperty = DependencyProperty.Register( - "ColumnIndex", typeof(int), typeof(FastGridViewColumn), new PropertyMetadata(-1)); + public static readonly DependencyProperty DisplayIndexProperty = DependencyProperty.Register( + "DisplayIndex", typeof(int), typeof(FastGridViewColumn), new PropertyMetadata(-1, (d,_) => OnPropertyChanged(d,"DisplayIndex"))); - public int ColumnIndex { - get { return (int)GetValue(ColumnIndexProperty); } - set { SetValue(ColumnIndexProperty, value); } + public int DisplayIndex { + get { return (int)GetValue(DisplayIndexProperty); } + set { SetValue(DisplayIndexProperty, value); } } internal bool IsResizingColumn { @@ -100,6 +102,15 @@ public bool? Sort { } } + public bool IsEditingFilter { + get => isEditingFilter_; + set { + if (value == isEditingFilter_) return; + isEditingFilter_ = value; + OnPropertyChanged(); + } + } + public bool IsSortAscending => Sort == true; public bool IsSortDescending => Sort == false; public bool IsSortNone => Sort == null; @@ -110,15 +121,17 @@ public bool? Sort { // note: not bindable at this time public bool IsSortable { get; set; } = true; + public FastGridViewFilterItem Filter { get; } = new FastGridViewFilterItem(); + // allow setting equivalence for this column + public PropertyValueCompareEquivalent FilterCompareEquivalent => Filter.CompareEquivalent; + // the idea: this is the name of the underlying property for this column. You only need to set this // if you want sorting and/or filtering // // note: not bindable at this time public string DataBindingPropertyName { get; set; } = ""; - public IComparable DataBindingSort { get; set; } = null; - public FastGridViewFilter DataBindingFilter { get; set; } = null; - public string FriendlyName() => UniqueName != "" ? UniqueName : ColumnIndex.ToString(); + public string FriendlyName() => UniqueName != "" ? UniqueName : DisplayIndex.ToString(); private static DataTemplate DefaultDataTemplate() { var dt = FastGridUtil.CreateDataTemplate(() => new Canvas()); @@ -160,6 +173,7 @@ public DataTemplate CellEditTemplate private bool? sort_ = null; private bool isResizingColumn_ = false; + private bool isEditingFilter_ = false; /// /// Gets or sets the data template for the cell in view mode. @@ -183,6 +197,7 @@ public DataTemplate CellTemplate public event PropertyChangedEventHandler PropertyChanged; + [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs b/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs index 63e553d..177e9bd 100644 --- a/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs +++ b/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs @@ -7,6 +7,8 @@ internal class FastGridViewColumnCollectionInternal : FastGridViewColumnCollecti private FastGridView _self; private List _oldColumns = new List(); + // if null -> force recreation + private List _sortedColumns = null; public FastGridViewColumnCollectionInternal(FastGridView self) { _self = self; @@ -21,6 +23,17 @@ private void FastGridViewColumnCollectionInternal_CollectionChanged(object sende _self.OnColumnsCollectionChanged(); } + private void BuildSortedColumns() { + if (_sortedColumns == null) + _sortedColumns = this.OrderBy(c => c.DisplayIndex).ToList(); + } + + public int GetColumnIndex(FastGridViewColumn column) { + BuildSortedColumns(); + var idx = FastGridUtil.RefIndex(_sortedColumns, column); + return idx; + } + private void Unsubscribe() { foreach (var col in _oldColumns) col.PropertyChanged -= Col_PropertyChanged; @@ -28,6 +41,12 @@ private void Unsubscribe() { private void Col_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { + switch (e.PropertyName) { + case "DisplayIndex": + _sortedColumns = null; + break; + } + _self.OnColumnPropertyChanged(sender as FastGridViewColumn, e.PropertyName); } diff --git a/src/FastControls/FastGrid/FastGridViewFilter.cs b/src/FastControls/FastGrid/FastGridViewFilter.cs deleted file mode 100644 index 3cd1ee5..0000000 --- a/src/FastControls/FastGrid/FastGridViewFilter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace FastGrid.FastGrid -{ - public class FastGridViewFilter { - private List _filterItems = new List(); - - public IReadOnlyList FilterItems => _filterItems; - - public void AddFilter(FastGridViewFilterItem filterItem) { - _filterItems.Add(filterItem); - } - - public IReadOnlyList GetFilterList(IReadOnlyList items, FastGridViewColumn col) - { - return null; - } - - public bool Matches(object obj) { - if (FilterItems.Count == 0) - return true; - - return FilterItems.All(f => f.Matches(obj)); - } - } -} diff --git a/src/FastControls/FastGrid/FastGridViewFilterItem.cs b/src/FastControls/FastGrid/FastGridViewFilterItem.cs deleted file mode 100644 index 6e9e1f1..0000000 --- a/src/FastControls/FastGrid/FastGridViewFilterItem.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; - -namespace FastGrid.FastGrid -{ - public class FastGridViewFilterItem : INotifyPropertyChanged - { - public string PropertyName { - get => propertyName_; - set { - if (value == propertyName_) return; - propertyName_ = value; - OnPropertyChanged(); - } - } - - public object PropertyValue { - get => propertyValue_; - set { - if (Equals(value, propertyValue_)) return; - propertyValue_ = value; - OnPropertyChanged(); - } - } - - public CompareType Compare { - get => compare_; - set { - if (value == compare_) return; - compare_ = value; - OnPropertyChanged(); - } - } - - // ... just in case we end up comparing doubles - public double Tolerance { get; set; } = 0.0000001; - - private PropertyInfo _propertyInfo; - private object propertyValue_ = null; - private string propertyName_ = ""; - private CompareType compare_ = CompareType.Equal; - - public enum CompareType { - Equal, Different, Less, Bigger, LessOrEqual, BiggerOrEqual, - StartsWith, EndsWith, Contains, - } - - public bool Matches(object obj) { - if (_propertyInfo == null && PropertyName != "") { - _propertyInfo = obj.GetType().GetProperty(PropertyName, BindingFlags.Public | BindingFlags.Instance); - } - - if (_propertyInfo == null) - return true; // could not get property? - - var objValue = _propertyInfo.GetValue(obj); - return MatchesValue(objValue, PropertyValue); - } - - private static double? TryGetDouble(object a) { - if (a is double) - return (double)a; - if (a is float) - return (float)a; - - return null; - } - private static long? TryGetInteger(object a) { - if (a is int) - return (int)a; - if (a is short) - return (short)a; - if (a is long) - return (long)a; - if (a is byte) - return (byte)a; - if (a is char) - return (char)a; - return null; - } - private static ulong? TryGetUnsignedInteger(object a) { - if (a is ushort) - return (ushort)a; - if (a is uint) - return (uint)a; - if (a is ulong) - return (ulong)a; - return null; - } - private DateTime? TryGetDate(object a) { - if (a is DateTime) - return (DateTime)a; - return null; - } - - private bool MatchesComparison(bool equal, bool less) { - switch (Compare) { - case CompareType.Equal: - return equal; - case CompareType.Different: - return !equal; - case CompareType.Less: - return less; - case CompareType.Bigger: - return !less && !equal; - case CompareType.LessOrEqual: - return less || equal; - case CompareType.BiggerOrEqual: - return equal || !less; - - case CompareType.StartsWith: - case CompareType.EndsWith: - case CompareType.Contains: - return false; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private bool MatchesDouble(object a, object b) { - var doubleA = TryGetDouble(a); - var doubleB = TryGetDouble(b); - - if (doubleA == null) { - var intA = TryGetInteger(a); - var uintA = TryGetUnsignedInteger(a); - if (intA != null) - doubleA = intA; - else if (uintA != null) - doubleA = uintA; - } - - if (doubleB == null) { - var intB = TryGetInteger(b); - var uintB = TryGetUnsignedInteger(b); - if (intB != null) - doubleB = intB; - else if (uintB != null) - doubleB = uintB; - } - - if (doubleA == null || doubleB == null) - return false; // one could convert to double, one could not - var equal = Math.Abs(doubleA.Value - doubleB.Value) < Tolerance; - var less = doubleA.Value < doubleB.Value; - return MatchesComparison(equal, less); - } - private bool MatchesUlong(object a, object b) { - var intA = TryGetUnsignedInteger(a); - var intB = TryGetUnsignedInteger(b); - if (intA == null || intB == null) - return false; - - var equal = intA.Value == intB.Value; - var less = intA.Value < intB.Value; - return MatchesComparison(equal, less); - } - private bool MatchesLong(object a, object b) { - var intA = TryGetInteger(a); - var intB = TryGetInteger(b); - if (intA == null || intB == null) - return false; - - var equal = intA.Value == intB.Value; - var less = intA.Value < intB.Value; - return MatchesComparison(equal, less); - } - - private bool MatchesDate(object a, object b) { - var dateA = TryGetDate(a); - var dateB = TryGetDate(b); - if (dateA == null || dateB == null) - return false; - var equal = dateA.Value == dateB.Value; - var less = dateA.Value == dateB.Value; - return MatchesComparison(equal, less); - } - - private bool MatchesString(object a, object b) { - var stringA = a.ToString(); - var stringB = b.ToString(); - - switch (Compare) { - case CompareType.StartsWith: - return stringA.StartsWith(stringB); - case CompareType.EndsWith: - return stringA.EndsWith(stringB); - case CompareType.Contains: - return stringA.IndexOf(stringB) >= 0; - } - - var compare = String.Compare(stringA, stringB, StringComparison.Ordinal); - var equal = compare == 0; - var less = compare < 0; - return MatchesComparison(equal, less); - } - - private bool MatchesValue(object objValue, object propertyValue) { - if (objValue == null || propertyValue == null) - return objValue == null && propertyValue == null; - - if (TryGetDouble(objValue) != null || TryGetDouble(propertyValue) != null) - return MatchesDouble(objValue, propertyValue); - if (TryGetInteger(objValue) != null || TryGetInteger(propertyValue) != null) - return MatchesLong(objValue, propertyValue); - if (TryGetUnsignedInteger(objValue) != null || TryGetUnsignedInteger(propertyValue) != null) - return MatchesUlong(objValue, propertyValue); - if (TryGetDate(objValue) != null || TryGetDate(propertyValue) != null) - return MatchesDate(objValue, propertyValue); - // long ulong double - // datetime - - // string - // object - convert to string - return MatchesString(objValue, propertyValue); - } - - - private void vm_propertyChanged(string name) { - _propertyInfo = null; - } - - public event PropertyChangedEventHandler PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { - vm_propertyChanged(propertyName); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/src/FastControls/FastGrid/FastGridViewRow.cs b/src/FastControls/FastGrid/FastGridViewRow.cs index 1ad3416..9235904 100644 --- a/src/FastControls/FastGrid/FastGridViewRow.cs +++ b/src/FastControls/FastGrid/FastGridViewRow.cs @@ -7,12 +7,13 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using OpenSilver.ControlsKit.Annotations; namespace FastGrid.FastGrid { internal class FastGridViewRow : Canvas, INotifyPropertyChanged { - private IReadOnlyList _columns; + private FastGridViewColumnCollectionInternal _columns; private List _cells = new List(); private bool _loaded = false; private double _rowHeight = 0; @@ -48,7 +49,7 @@ internal FrameworkElement RowContentChild { } } - public FastGridViewRow(DataTemplate rowTemplate, IReadOnlyList columnInfo, double rowHeight) { + public FastGridViewRow(DataTemplate rowTemplate, FastGridViewColumnCollectionInternal columnInfo, double rowHeight) { CustomLayout = true; _columns = columnInfo; RowHeight = rowHeight; @@ -88,10 +89,8 @@ private void Load() { // create the cells var offset = 0d; foreach (var ci in _columns) { - var cc = new FastGridViewCell { + var cc = new FastGridViewCell(ci) { ContentTemplate = ci.CellTemplate, - IsCellVisible = ci.IsVisible, - CellIndex = ci.ColumnIndex, Width = ci.Width, MinWidth = ci.MinWidth, MaxWidth = ci.MaxWidth, @@ -189,6 +188,7 @@ internal void UpdateUI() { var x = -HorizontalOffset; foreach (var cell in _cells) { var offset = cell.IsCellVisible ? x : -100000; + cell.UpdateWidth(); FastGridUtil.SetLeft(cell, offset); if (cell.IsCellVisible) x += cell.Width; @@ -196,32 +196,6 @@ internal void UpdateUI() { } - internal void SetCellVisible(FastGridViewColumn column, bool isVisible) { - var idx = FastGridUtil.RefIndex(_columns, column); - Debug.Assert(idx >= 0); - _cells[idx].IsCellVisible = isVisible; - } - internal void SetCellWidth(FastGridViewColumn column, double width) { - var idx = FastGridUtil.RefIndex(_columns, column); - Debug.Assert(idx >= 0); - FastGridUtil.SetWidth(_cells[idx], width); - } - internal void SetCellMinWidth(FastGridViewColumn column, double width) { - var idx = FastGridUtil.RefIndex(_columns, column); - Debug.Assert(idx >= 0); - _cells[idx].MinWidth = width; - } - internal void SetCellMaxWidth(FastGridViewColumn column, double width) { - var idx = FastGridUtil.RefIndex(_columns, column); - Debug.Assert(idx >= 0); - _cells[idx].MaxWidth = width; - } - internal void SetCellIndex(FastGridViewColumn column, int index) { - var idx = FastGridUtil.RefIndex(_columns, column); - Debug.Assert(idx >= 0); - _cells[idx].CellIndex = index; - } - private void BackgroundChanged() { _selection.Background = IsSelected ? SelectedBrush : _transparent; @@ -233,8 +207,6 @@ protected override void OnMouseLeftButtonDown(MouseButtonEventArgs eventArgs) { } - - private void vm_PropertyChanged(string propertyName) { switch (propertyName) { case "RowHeight": @@ -255,6 +227,7 @@ private void vm_PropertyChanged(string propertyName) { public event PropertyChangedEventHandler PropertyChanged; + [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { if (_loaded) vm_PropertyChanged(propertyName); diff --git a/src/FastControls/FastGrid/Filter/FastGridViewFilter.cs b/src/FastControls/FastGrid/Filter/FastGridViewFilter.cs new file mode 100644 index 0000000..7205773 --- /dev/null +++ b/src/FastControls/FastGrid/Filter/FastGridViewFilter.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewFilter { + private List _filterItems = new List(); + + public IReadOnlyList FilterItems => _filterItems; + + public void AddFilter(FastGridViewFilterItem filterItem) { + Debug.Assert(filterItem.PropertyName != ""); + var foundIdx = _filterItems.FindIndex(fi => fi.PropertyName == filterItem.PropertyName); + if (foundIdx >= 0) + _filterItems[foundIdx] = filterItem; + else + _filterItems.Add(filterItem); + } + + public void RemoveFilter(FastGridViewFilterItem filterItem) { + RemoveFilter(filterItem.PropertyName); + } + public void RemoveFilter(string propertyName) { + _filterItems.RemoveAll(fi => fi.PropertyName == propertyName); + } + + public FastGridViewFilter Copy() { + return new FastGridViewFilter { + _filterItems = _filterItems.ToList(), + }; + } + + public FastGridViewFilterItem GetOrAddFilterForProperty(FastGridViewColumn col) => GetOrAddFilterForProperty(col.DataBindingPropertyName); + public FastGridViewFilterItem GetOrAddFilterForProperty(string name) { + var found = _filterItems.FirstOrDefault(fi => fi.PropertyName == name); + if (found != null) + return found; + else { + var added = new FastGridViewFilterItem { + PropertyName = name, + }; + AddFilter(added); + return added; + } + } + + public FastGridViewFilterItem TryFilterForProperty(FastGridViewColumn col) => TryFilterForProperty(col.DataBindingPropertyName); + public FastGridViewFilterItem TryFilterForProperty(string name) { + return _filterItems.FirstOrDefault(fi => fi.PropertyName == name); + } + + public IReadOnlyList GetFilterList(IReadOnlyList items, FastGridViewColumn col) + { + return null; + } + + public bool IsEmpty => FilterItems.Count == 0 || FilterItems.Count(f => !f.IsEmpty) == 0; + + public bool Matches(object obj) { + if (FilterItems.Count == 0) + return true; + + return FilterItems.All(f => f.Matches(obj)); + } + } +} diff --git a/src/FastControls/FastGrid/Filter/FastGridViewFilterCtrl.xaml b/src/FastControls/FastGrid/Filter/FastGridViewFilterCtrl.xaml new file mode 100644 index 0000000..22bd35b --- /dev/null +++ b/src/FastControls/FastGrid/Filter/FastGridViewFilterCtrl.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + +