diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..2fb2b21 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,59 @@ +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/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..bca21d5 --- /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..c3018eb --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Interop/UnmarshalledJavaScriptExecutionHandler.cs @@ -0,0 +1,36 @@ +using DotNetForHtml5; +using Microsoft.JSInterop; +using Microsoft.JSInterop.WebAssembly; + +namespace FastControls.TestApp.Browser.Interop +{ + public class UnmarshalledJavaScriptExecutionHandler : IWebAssemblyExecutionHandler + { + 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); + } + public TResult InvokeUnmarshalled(string identifier, T0 arg0) + { + return _runtime.InvokeUnmarshalled(identifier, arg0); + } + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) + { + return _runtime.InvokeUnmarshalled(identifier, arg0, arg1); + } + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) + { + return _runtime.InvokeUnmarshalled(identifier, arg0, arg1, arg2); + } + } +} \ 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..5ed83e2 --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp.Browser/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" + }, + "FastControls.TestApp.Browser": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:55595", + "hotReloadEnabled": false + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55591/", + "sslPort": 0 + } + } +} \ 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 0000000..a4bff57 Binary files /dev/null and b/src/FastControls.TestApp/FastControls.TestApp.Browser/wwwroot/favicon.ico differ 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..df11e09 --- /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..3a667d1 --- /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; +using FastGrid.FastGrid; + +namespace FastControls.TestApp +{ + public sealed partial class App : Application + { + public App() + { + this.InitializeComponent(); + + // Enter construction logic here... + + 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 new file mode 100644 index 0000000..9937bdd --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/FastControls.TestApp.csproj @@ -0,0 +1,42 @@ + + + + netstandard2.0 + false + true + + + + + + + + + MSBuild:Compile + + + 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/MockViewModel.cs b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs new file mode 100644 index 0000000..6c191ca --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/MockViewModel.cs @@ -0,0 +1,191 @@ +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 DummyDate : 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; + private DateTime time_ = DateTime.Now; + + 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 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) { + 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; + + [NotifyPropertyChangedInvocator] + 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) { + 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 , + Username = $"User {i}", + 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/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.TestApp/FastControls.TestApp/TestFastGridView.xaml b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml new file mode 100644 index 0000000..39f6c1f --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..11b83bc --- /dev/null +++ b/src/FastControls.TestApp/FastControls.TestApp/TestFastGridView.xaml.cs @@ -0,0 +1,286 @@ +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; + +namespace FastGrid.FastGrid +{ + public partial class TestFastGridView : Page + { + private const short ROW_COUNT = 10; + private ObservableCollection _pullouts; + public TestFastGridView() + { + this.InitializeComponent(); + } + + private async Task TestSimulateScroll() + { + for (int i = 0; i < ROW_COUNT; ++i) + { + ctrl.VerticalScrollToRowIndex(i + 1); + await Task.Delay(50); + } + for (int i = ROW_COUNT; i >= 0; --i) + { + ctrl.VerticalScrollToRowIndex(i + 1); + await Task.Delay(50); + } + } + + private int RefIndex(DummyDate dummyDate) + { + int idx = 0; + _pullouts.FirstOrDefault(i => + { + if (ReferenceEquals(i, dummyDate)) + 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 DummyDate; + 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(5)); + 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))}"); + }; + } + // 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)); + ctrl.ItemsSource = _pullouts; + for (int i = 0; i < 50; ++i) { + await Task.Delay(4000); + Canvas.SetLeft(ctrl, (i % 2) == 0 ? -10000 : 0); + } + } + + 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 void Page_Loaded(object sender, RoutedEventArgs e) { + + ctrl.Columns["Time"].FilterCompareEquivalent.DateTimeFormat = "HH:mm"; + SimpleTest(); + //SimpleTestFilter(); + //SimpleTestFilterFewItmes(); + //await TestChangeColumnOrder(); + //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/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/FastCheckBox.cs b/src/FastControls/FastCheckBox.cs new file mode 100644 index 0000000..a2f0faa --- /dev/null +++ b/src/FastControls/FastCheckBox.cs @@ -0,0 +1,256 @@ +/*=================================================================================== +* +* 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 CSHTML5.Internal; + +namespace OpenSilver.ControlsKit +{ + public class FastCheckBox : FrameworkElement + { + 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 + }); + }), true); + // 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/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..c4c7a23 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridContentTemplate.cs @@ -0,0 +1,251 @@ +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; +using DotNetForHtml5.Core; + +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 SortGeometry() { + 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 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; + /* + + + + */ + var grid = new Grid { + Background = new SolidColorBrush(Colors.Transparent), + }; + var tb = new TextBlock { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = headerMargin, + 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 = SortPath(); + var filterButton = new ContentControl { + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 10, 0), + Padding = new Thickness(2), + Content = FilterPath(), + Cursor = Cursors.Hand, + }; + + 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(filterButton); + 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); + }; + + 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; + 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; + }; + + 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; + } + + 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..fc89c88 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridView.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FastControls/FastGrid/FastGridView.xaml.cs b/src/FastControls/FastGrid/FastGridView.xaml.cs new file mode 100644 index 0000000..3052af8 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridView.xaml.cs @@ -0,0 +1,1596 @@ +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 FastGrid.FastGrid.Filter; +using OpenSilver; +using OpenSilver.ControlsKit.Annotations; + +namespace FastGrid.FastGrid +{ + /* + * + * 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 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 + // 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; + private bool _needsRebuildHeader = false; + + public enum RightClickAutoSelectType { + None, Select, SelectAdd, + } + + 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 => _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; + 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; + + 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 + 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; + + 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), + 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(36d, 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); } + } + + 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(); + } + 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; + } + + // 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) { + // nothing to draw + _topRow = null; + _topRowIndexWhenNotScrolling = 0; + return; + } + + var foundIdx = ObjectTo_Top_RowIndex(_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 (_items == 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 (_needsRebuildHeader) { + _needsRebuildHeader = false; + RebuildHeaderCollection(); + } + + if (_needsRefilter) { + _needsRefilter = false; + FullReFilter(); + } + + if (_needsFullReSort) { + _needsFullReSort = false; + _needsReSort = false; + _sort.FullResort(); + } + if (_needsReSort) { + _needsReSort = false; + _sort.FastResort(); + } + + 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); + 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: + 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; + Console.WriteLine($"Fastgrid {Name} - needs refilter/resort"); + break; + } + } + + PostponeUpdateUI(); + } + + private bool MatchesFilter(object item) { + if (!IsFilteringAllowed) + return true; // no filtering + return Filter.Matches(item); + } + + 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; + 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() + { + 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(); + CreateFilter(); + CreateHeader(); + HandleContextMenu(); + + HeaderHeightChanged(); + RowHeightChanged(); + 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; + + // 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; + 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; + } + + 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; + private object rightClickSelectedObject_ = null; + + internal void OnColumnPropertyChanged(FastGridViewColumn col, string propertyName) { + switch (propertyName) { + case "IsVisible": + PostponeUpdateUI(); + break; + + case "IsResizingColumn": + if (col.IsResizingColumn) { + if (!InstantColumnResize) + SetRowOpacity(0.4); + } else { + if (!InstantColumnResize) { + // first, make sure all cells have the correct size, after row resize + foreach (var row in _rows) + row.UpdateUI(); + + SetRowOpacity(1); + 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.UpdateUI(); + PostponeUpdateUI(); + } + break; + + case "MinWidth": + PostponeUpdateUI(); + break; + + case "MaxWidth": + PostponeUpdateUI(); + break; + + case "DisplayIndex": + _needsRebuildHeader = true; + 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 "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; + 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 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": + OnHorizontalOffsetChange(); + break; + case "IsScrollingHorizontally": + if (!IsScrollingHorizontally) + // end of scroll + HorizontalOffset = horizontalScrollbar.Value; + break; + case "IsOffscreen": + OnOffscreenChange(); + break; + } + } + + 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 new file mode 100644 index 0000000..6f50a41 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewCell.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +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 bool isCellVisible_ = true; + + // the reason for this - much easier to resort, when the column's display index changes + private FastGridViewColumn column_; + + public bool IsCellVisible => column_.IsVisible; + public int CellIndex => column_.DisplayIndex; + + public FastGridViewCell(FastGridViewColumn column) { + column_ = column; + 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 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 new file mode 100644 index 0000000..eb67e62 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewColumn.cs @@ -0,0 +1,205 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using FastGrid.FastGrid.Filter; +using OpenSilver.ControlsKit.Annotations; + +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 DisplayIndexProperty = DependencyProperty.Register( + "DisplayIndex", typeof(int), typeof(FastGridViewColumn), new PropertyMetadata(-1, (d,_) => OnPropertyChanged(d,"DisplayIndex"))); + + public int DisplayIndex { + get { return (int)GetValue(DisplayIndexProperty); } + set { SetValue(DisplayIndexProperty, 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 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; + 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; + + 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 string FriendlyName() => UniqueName != "" ? UniqueName : DisplayIndex.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; + private bool isEditingFilter_ = 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; + + [NotifyPropertyChangedInvocator] + 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..177e9bd --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewColumnCollectionInternal.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewColumnCollectionInternal : FastGridViewColumnCollection { + private FastGridView _self; + + private List _oldColumns = new List(); + // if null -> force recreation + private List _sortedColumns = null; + + 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 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; + } + + 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); + } + + private void Subscribe() { + foreach (var col in _oldColumns) + col.PropertyChanged += Col_PropertyChanged; + } + } +} diff --git a/src/FastControls/FastGrid/FastGridViewRow.cs b/src/FastControls/FastGrid/FastGridViewRow.cs new file mode 100644 index 0000000..9235904 --- /dev/null +++ b/src/FastControls/FastGrid/FastGridViewRow.cs @@ -0,0 +1,237 @@ +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; +using OpenSilver.ControlsKit.Annotations; + +namespace FastGrid.FastGrid +{ + internal class FastGridViewRow : Canvas, INotifyPropertyChanged { + + private FastGridViewColumnCollectionInternal _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, FastGridViewColumnCollectionInternal 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(ci) { + ContentTemplate = ci.CellTemplate, + 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; + cell.UpdateWidth(); + FastGridUtil.SetLeft(cell, offset); + if (cell.IsCellVisible) + x += cell.Width; + } + } + + + 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; + + [NotifyPropertyChangedInvocator] + 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/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 @@ + + + + + + + + + + + + + + + + + + + +