mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-13 21:30:44 +00:00
Compare commits
244 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae64ba919 | ||
|
|
6e94a7c65b | ||
|
|
44e20b2b5f | ||
|
|
d109c2e295 | ||
|
|
0feffad278 | ||
|
|
3be412643f | ||
|
|
6d8c4ceb16 | ||
|
|
40b7d84224 | ||
|
|
4511c654f9 | ||
|
|
547a681878 | ||
|
|
cad80d533d | ||
|
|
f485420ba9 | ||
|
|
ac61235a48 | ||
|
|
61bd1db8b9 | ||
|
|
5992a97ef9 | ||
|
|
157070c922 | ||
|
|
14e39f665f | ||
|
|
2f77a0b849 | ||
|
|
c3af020b81 | ||
|
|
19460b899b | ||
|
|
3cf12c82bf | ||
|
|
adf86b2896 | ||
|
|
7b96fe3cb4 | ||
|
|
9a58c3a35a | ||
|
|
876c248a8e | ||
|
|
0ce1c06178 | ||
|
|
16f4f1839a | ||
|
|
b5e30cbfe0 | ||
|
|
604fdfb28a | ||
|
|
d5b40d5e9b | ||
|
|
13e1a55ee8 | ||
|
|
28e9090d45 | ||
|
|
d03020062e | ||
|
|
fc6fe54635 | ||
|
|
505ffa7fe4 | ||
|
|
4f82ac9599 | ||
|
|
5bbf7c1fde | ||
|
|
e416cede62 | ||
|
|
270a7e0f80 | ||
|
|
1a12a75bde | ||
|
|
d1917eac0c | ||
|
|
09830f0ea2 | ||
|
|
d34e0684fb | ||
|
|
482eca3278 | ||
|
|
5c8ac16c83 | ||
|
|
29382cf0f4 | ||
|
|
53204cc8d6 | ||
|
|
6cf550c517 | ||
|
|
7bf107af4c | ||
|
|
adf2785a5a | ||
|
|
cae44df00a | ||
|
|
8ba1c69573 | ||
|
|
56031c2fd4 | ||
|
|
ad44802d30 | ||
|
|
64ca088e61 | ||
|
|
f4777565a2 | ||
|
|
eb1423e757 | ||
|
|
596b251511 | ||
|
|
e186315935 | ||
|
|
cba9a5120d | ||
|
|
ee62a21ab6 | ||
|
|
0a5e318f80 | ||
|
|
8dd7d7f521 | ||
|
|
69573b2d7d | ||
|
|
5b933c6643 | ||
|
|
126b2d1efa | ||
|
|
a65c1a9482 | ||
|
|
4939e8498a | ||
|
|
e380467853 | ||
|
|
d3adc9733b | ||
|
|
a03fc50ee8 | ||
|
|
c9201bf947 | ||
|
|
8c8d288afd | ||
|
|
1a679af008 | ||
|
|
bc1654a405 | ||
|
|
94ef62e00b | ||
|
|
13afd0fddb | ||
|
|
fa7120571d | ||
|
|
4d4e882dcc | ||
|
|
af8c7179d1 | ||
|
|
c743054184 | ||
|
|
935d850296 | ||
|
|
ad6b6886f1 | ||
|
|
d5d08c89de | ||
|
|
5a246f732e | ||
|
|
669ad421d9 | ||
|
|
98e8393d14 | ||
|
|
de8dfb7e93 | ||
|
|
61b32d6490 | ||
|
|
5057fb1cc8 | ||
|
|
ed49dfc2fa | ||
|
|
839018795c | ||
|
|
c541f9d3e5 | ||
|
|
5e5957e4c4 | ||
|
|
84a759267e | ||
|
|
eb1e6fab0d | ||
|
|
6094953009 | ||
|
|
b845c73ad6 | ||
|
|
2788606105 | ||
|
|
aebf267320 | ||
|
|
33896ea8db | ||
|
|
cb8699f315 | ||
|
|
2a43db6560 | ||
|
|
0ad1200870 | ||
|
|
2debdbfd38 | ||
|
|
07c96bd4bd | ||
|
|
704c6abe7c | ||
|
|
e0e0e608e2 | ||
|
|
40e15a0720 | ||
|
|
d6235fd147 | ||
|
|
c5a3a911e0 | ||
|
|
4e7a037bc5 | ||
|
|
f1c3f46ad7 | ||
|
|
6c0e39da20 | ||
|
|
2a9d638acf | ||
|
|
426e1ba8e8 | ||
|
|
1bb36ef207 | ||
|
|
67cde0fd59 | ||
|
|
0696cd20d5 | ||
|
|
34fce5188f | ||
|
|
273ba0381f | ||
|
|
63a05d86e8 | ||
|
|
283e115d0a | ||
|
|
b513ebba8e | ||
|
|
e324cea7b9 | ||
|
|
c3cda91a2d | ||
|
|
bb9f611629 | ||
|
|
6cc8e62b15 | ||
|
|
5a44b76adb | ||
|
|
109d6bc617 | ||
|
|
7249ff6107 | ||
|
|
406830fa13 | ||
|
|
a6a3278443 | ||
|
|
9fd8531529 | ||
|
|
c687976796 | ||
|
|
ae511929f7 | ||
|
|
e78e756206 | ||
|
|
2e9e0dac1a | ||
|
|
bff97712e2 | ||
|
|
78bc25cce0 | ||
|
|
a2e829417d | ||
|
|
9902413daf | ||
|
|
06a35f4a73 | ||
|
|
e2a46157b9 | ||
|
|
c4913a94c4 | ||
|
|
d6e04c3ae8 | ||
|
|
5b17ef5217 | ||
|
|
8edeedc32c | ||
|
|
543dbc9e50 | ||
|
|
3710f4297b | ||
|
|
91d6ba4da8 | ||
|
|
fbfa3c1abe | ||
|
|
d13d7bef0c | ||
|
|
510f5d7190 | ||
|
|
595b98d4b7 | ||
|
|
c696107aeb | ||
|
|
632722cd7b | ||
|
|
60c264ea49 | ||
|
|
10ce92264d | ||
|
|
2e5c7aa6bc | ||
|
|
1e7e2c5b51 | ||
|
|
46790ce3fe | ||
|
|
7bd891f8f0 | ||
|
|
1ae16acbac | ||
|
|
2281fb7f61 | ||
|
|
89e8db6c6e | ||
|
|
11300692e5 | ||
|
|
a31161c6d8 | ||
|
|
edd85f8ec0 | ||
|
|
0d420604dd | ||
|
|
777f5666e2 | ||
|
|
e7bded641f | ||
|
|
6f0dfbe038 | ||
|
|
0e92eeb50e | ||
|
|
50b6cb879a | ||
|
|
739ebde6a8 | ||
|
|
24ef74f16d | ||
|
|
9e0a57ca5b | ||
|
|
feaebb6f0f | ||
|
|
9a45759414 | ||
|
|
14c5a0447b | ||
|
|
7793e03d82 | ||
|
|
f422573fcc | ||
|
|
516109ecd3 | ||
|
|
31d32b16a5 | ||
|
|
435c1bd6e2 | ||
|
|
6d5ebf3f58 | ||
|
|
b97036b447 | ||
|
|
d7ef3cb896 | ||
|
|
2218eefa67 | ||
|
|
666d33d767 | ||
|
|
3f4f83d354 | ||
|
|
3dea547559 | ||
|
|
0e9c6acb84 | ||
|
|
1b6881673e | ||
|
|
f069b33b60 | ||
|
|
c0a86e31da | ||
|
|
7b95778efe | ||
|
|
ef8a102d0a | ||
|
|
95448de3ed | ||
|
|
0a574762c7 | ||
|
|
59b1440990 | ||
|
|
98d6729d4b | ||
|
|
135a0bbe5c | ||
|
|
c3f579931d | ||
|
|
21c51e81d2 | ||
|
|
e6538c95ad | ||
|
|
0e63e87f9b | ||
|
|
1e900ec775 | ||
|
|
5ff30874dd | ||
|
|
94550006c4 | ||
|
|
202636ce72 | ||
|
|
cae8c6f622 | ||
|
|
0e03c4377f | ||
|
|
7497ea1262 | ||
|
|
d68bb34f6f | ||
|
|
732a6f4942 | ||
|
|
70fb896ae1 | ||
|
|
034eae6722 | ||
|
|
3472949bf0 | ||
|
|
c333b8ca30 | ||
|
|
17e3fbdabf | ||
|
|
30c5c9dfaf | ||
|
|
585d1ee38a | ||
|
|
3255afb487 | ||
|
|
9f75648b50 | ||
|
|
50406f8984 | ||
|
|
f24c7ebc5f | ||
|
|
75dcecfab0 | ||
|
|
34c603ce53 | ||
|
|
7785a73876 | ||
|
|
cfa8f731f2 | ||
|
|
5ecd05c7b3 | ||
|
|
4ec95c2f1c | ||
|
|
9e4413b02e | ||
|
|
38d0d689b3 | ||
|
|
799c5e9e4e | ||
|
|
b98dffda8f | ||
|
|
d816d841a8 | ||
|
|
1730e02dc3 | ||
|
|
53737d2f3c | ||
|
|
932dc3f9f6 | ||
|
|
567fdb6a36 | ||
|
|
3e323b929d |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,10 +11,10 @@ assignees: ''
|
||||
|
||||
IMPORTANT: Read this first!!!
|
||||
|
||||
1. If you own a Radzen Blazor subscription you can report your issue or ask us a question via email at info@radzen.com. Radzen staff will reply within 24 hours (Pro) or 16 hours (Team)
|
||||
1. If you own a Radzen Blazor Pro or Team subscription you can also report your issue or ask us a question via email at info@radzen.com. Radzen staff will reply within 24 hours (Pro) or 16 hours (Team)
|
||||
2. The Radzen staff guarantees a response to issues in this repo only to paid subscribers.
|
||||
3. If you have a HOW TO question start a new forum thread in the Radzen Community forum: https://forum.radzen.com. Radzen staff will close issues that are HOWTO questions.
|
||||
4. Please adhere to the issue template. Specify all the steps required to reproduce the issue or link a project which reproduces it easily (without requiring extra steps such as restoring a database).
|
||||
4. Please adhere to the issue template. Specify all the steps required to reproduce the issue.
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
@@ -27,7 +27,12 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
Alternatively link your repo with a sample project that can be run.
|
||||
Alternatively make a new [playground](https://blazor.radzen.com/playground) snippet and paste its URL.
|
||||
1. Go to any live demo at https://blazor.radzen.com
|
||||
2. Click the **Edit Source** tab.
|
||||
3. Then click **Open in Playground**.
|
||||
4. Reproduce the problem and save the snippet.
|
||||
5. Copy the snippet URL and provide it in the issue description.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build Radzen.Blazor/Radzen.Blazor.csproj
|
||||
|
||||
66
.github/workflows/deploy.yml
vendored
Normal file
66
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Deploy to blazor.radzen.com
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: blazor-radzen-prod
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Compute image tag (timestamp)
|
||||
id: meta
|
||||
run: |
|
||||
TAG=$(date -u +"%Y%m%d%H%M%S")
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}"
|
||||
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.image }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Push to dokku
|
||||
uses: dokku/github-action@master
|
||||
with:
|
||||
git_remote_url: ${{ secrets.DOKKU_REPO }}
|
||||
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
|
||||
deploy_docker_image: ${{ steps.meta.outputs.image }}
|
||||
|
||||
- name: Prune GHCR versions (keep 1)
|
||||
uses: actions/delete-package-versions@v5
|
||||
with:
|
||||
package-name: radzen-blazor
|
||||
package-type: container
|
||||
min-versions-to-keep: 1
|
||||
delete-only-pre-release-versions: false
|
||||
85
Directory.Build.props
Normal file
85
Directory.Build.props
Normal file
@@ -0,0 +1,85 @@
|
||||
<Project>
|
||||
<!--
|
||||
Common build properties for all projects in the Radzen.Blazor solution.
|
||||
|
||||
To use this file:
|
||||
1. Rename to Directory.Build.props (remove .sample extension)
|
||||
2. Adjust settings based on your needs
|
||||
3. Review the analyzer settings in .editorconfig
|
||||
|
||||
This file will be automatically imported by all projects in subdirectories.
|
||||
-->
|
||||
|
||||
<PropertyGroup Label="Language Configuration">
|
||||
<!-- Use latest C# language features -->
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- Do NOT enable implicit usings - explicit imports preferred for library code -->
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Code Analysis Configuration">
|
||||
<!-- Enable .NET code analyzers -->
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
|
||||
<!-- Run analyzers during build and in IDE -->
|
||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
|
||||
|
||||
<!-- Don't enforce code style in build (yet) - just show warnings -->
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
|
||||
<!-- Don't treat warnings as errors (yet) - too many to fix immediately -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
|
||||
<!-- Report all analyzer diagnostics -->
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Build Quality">
|
||||
<!-- Enable deterministic builds for reproducibility -->
|
||||
<Deterministic>true</Deterministic>
|
||||
|
||||
<!-- Enable deterministic builds in CI/CD -->
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Embed source files for better debugging -->
|
||||
<EmbedAllSources>true</EmbedAllSources>
|
||||
<!--
|
||||
IMPORTANT:
|
||||
- NuGet symbol packages (.snupkg) require portable PDB files.
|
||||
- If DebugType=embedded, there are no standalone PDBs, so the .snupkg ends up effectively empty.
|
||||
Use portable PDBs when symbols are enabled; otherwise use embedded for local debugging convenience.
|
||||
-->
|
||||
<!--
|
||||
NOTE: Directory.Build.props is imported before project files, so properties like IncludeSymbols
|
||||
set in a .csproj may not be available yet for Conditions here.
|
||||
IsPacking *is* set by `dotnet pack`, so use that to switch DebugType for symbol packages.
|
||||
-->
|
||||
<DebugType Condition="'$(IsPacking)' == 'true'">portable</DebugType>
|
||||
<DebugType Condition="'$(IsPacking)' != 'true'">embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Demos and Tests Project Configuration" Condition="$(MSBuildProjectName.Contains('Demos')) OR $(MSBuildProjectName.Contains('Tests'))">
|
||||
<!-- Demo projects and Tests should not be packable -->
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<!-- DISABLE ALL ANALYZERS FOR DEMO PROJECTS AND TESTS -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Performance">
|
||||
<!-- Optimize startup time -->
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
49
Dockerfile
49
Dockerfile
@@ -1,23 +1,48 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0
|
||||
# =============================
|
||||
# BUILD STAGE
|
||||
# =============================
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY Radzen.Blazor /app/Radzen.Blazor
|
||||
COPY Radzen.DocFX /app/Radzen.DocFX
|
||||
COPY RadzenBlazorDemos /app/RadzenBlazorDemos
|
||||
COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
|
||||
# Copy project files first for better caching
|
||||
COPY Radzen.Blazor/*.csproj Radzen.Blazor/
|
||||
COPY RadzenBlazorDemos/*.csproj RadzenBlazorDemos/
|
||||
COPY RadzenBlazorDemos.Host/*.csproj RadzenBlazorDemos.Host/
|
||||
|
||||
WORKDIR /app
|
||||
# Radzen.DocFX usually has no csproj → copy full folder
|
||||
COPY Radzen.DocFX/ Radzen.DocFX/
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore RadzenBlazorDemos.Host/RadzenBlazorDemos.Host.csproj
|
||||
|
||||
# Copy full source after restore layer
|
||||
COPY . .
|
||||
|
||||
# Install docfx (build stage only)
|
||||
RUN dotnet tool install -g docfx
|
||||
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||
RUN wget https://dot.net/v1/dotnet-install.sh \
|
||||
&& bash dotnet-install.sh --channel 8.0 --runtime dotnet --install-dir /usr/share/dotnet
|
||||
|
||||
# Build shared project (keep net8.0 if required)
|
||||
RUN dotnet build -c Release Radzen.Blazor/Radzen.Blazor.csproj -f net8.0
|
||||
|
||||
# Generate documentation
|
||||
RUN docfx Radzen.DocFX/docfx.json
|
||||
|
||||
WORKDIR /app/RadzenBlazorDemos.Host
|
||||
RUN dotnet publish -c Release -o out
|
||||
# Publish the Blazor host app
|
||||
WORKDIR /src/RadzenBlazorDemos.Host
|
||||
RUN dotnet publish -c Release -o /app/out
|
||||
|
||||
ENV ASPNETCORE_URLS=http://*:5000
|
||||
WORKDIR /app/RadzenBlazorDemos.Host/out
|
||||
|
||||
# =============================
|
||||
# RUNTIME STAGE
|
||||
# =============================
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only published output
|
||||
COPY --from=build /app/out ./
|
||||
|
||||
# Set runtime URL
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
|
||||
ENTRYPOINT ["dotnet", "RadzenBlazorDemos.Host.dll"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2025 Radzen Ltd
|
||||
Copyright (c) 2018-2026 Radzen Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -54,4 +54,4 @@ Check the [getting started](https://blazor.radzen.com/get-started) instructions
|
||||
|
||||
## Run demos locally
|
||||
|
||||
Use Radzen.Server.sln to open and run demos as Blazor server application or Radzen.WebAssembly.sln to open and run demos as Blazor WebAssembly application. Radzen.sln has reference to all projects including tests.
|
||||
Use **Radzen.Server.sln** to open and run demos as Blazor server application or **Radzen.WebAssembly.sln** to open and run demos as Blazor WebAssembly application. The demos require the .NET 10 SDK and should preferably be opened in VS2026.
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
// Find and click the accordion header link to expand
|
||||
var header = component.Find(".rz-accordion-header a");
|
||||
var header = component.Find(".rz-accordion-header button");
|
||||
header.Click();
|
||||
|
||||
Assert.True(expandRaised);
|
||||
@@ -156,7 +156,7 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
// Find and click the accordion header link to collapse
|
||||
var header = component.Find(".rz-accordion-header a");
|
||||
var header = component.Find(".rz-accordion-header button");
|
||||
header.Click();
|
||||
|
||||
Assert.True(collapseRaised);
|
||||
@@ -184,7 +184,7 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
// Try to click the disabled item
|
||||
var header = component.Find(".rz-accordion-header a");
|
||||
var header = component.Find(".rz-accordion-header button");
|
||||
header.Click();
|
||||
|
||||
// Event should not be raised for disabled item
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Bunit;
|
||||
using System.Collections;
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
@@ -135,5 +138,30 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Equal("additional-name", AutoCompleteType.MiddleName.GetAutoCompleteValue());
|
||||
Assert.Equal("family-name", AutoCompleteType.LastName.GetAutoCompleteValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AutoComplete_Filters_StringList()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Apple", "Banana", "Cherry" };
|
||||
|
||||
var component = ctx.RenderComponent<AutoCompleteWithAccessibleView>(parameters =>
|
||||
{
|
||||
parameters
|
||||
.Add(p => p.Data, data)
|
||||
.Add(p => p.SearchText, "Ban")
|
||||
.Add(p => p.OpenOnFocus, true);
|
||||
});
|
||||
|
||||
Assert.Contains("Banana", component.Instance.CurrentView.OfType<string>());
|
||||
Assert.DoesNotContain("Apple", component.Instance.CurrentView.OfType<string>());
|
||||
Assert.DoesNotContain("Cherry", component.Instance.CurrentView.OfType<string>());
|
||||
}
|
||||
|
||||
private sealed class AutoCompleteWithAccessibleView : RadzenAutoComplete
|
||||
{
|
||||
public IEnumerable CurrentView => View;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1728,6 +1728,7 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Name");
|
||||
builder.AddAttribute(2, "Type", typeof(string));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
|
||||
@@ -1751,6 +1752,7 @@ namespace Radzen.Blazor.Tests
|
||||
{
|
||||
builder.OpenComponent(0, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(1, "Property", "Name");
|
||||
builder.AddAttribute(1, "Type", typeof(string));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
|
||||
@@ -2804,6 +2806,12 @@ namespace Radzen.Blazor.Tests
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent(3, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(4, "Property", "Tags");
|
||||
builder.AddAttribute(5, "Title", "Tags");
|
||||
builder.AddAttribute(6, "Type", typeof(object[]));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
|
||||
parameterBuilder.Add<FilterMode>(p => p.FilterMode, FilterMode.SimpleWithMenu);
|
||||
@@ -2834,6 +2842,12 @@ namespace Radzen.Blazor.Tests
|
||||
builder.AddAttribute(1, "Property", "Id");
|
||||
builder.AddAttribute(2, "Title", "Id");
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent(3, typeof(RadzenDataGridColumn<dynamic>));
|
||||
builder.AddAttribute(4, "Property", "Tags");
|
||||
builder.AddAttribute(5, "Title", "Tags");
|
||||
builder.AddAttribute(6, "Type", typeof(object[]));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
parameterBuilder.Add<bool>(p => p.AllowFiltering, true);
|
||||
parameterBuilder.Add<FilterMode>(p => p.FilterMode, FilterMode.SimpleWithMenu);
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<bool>(p => p.AllowClear, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"<i class=""notranslate rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
|
||||
Assert.Contains(@$"<button type=""button"" class=""notranslate rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using System;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class DialogServiceTests
|
||||
public class DialogServiceTests : ComponentBase
|
||||
{
|
||||
public class OpenDialogTests
|
||||
{
|
||||
@@ -124,13 +128,81 @@ namespace Radzen.Blazor.Tests
|
||||
var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []);
|
||||
dialogService.Close();
|
||||
await openTask;
|
||||
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Dynamic Open", resultingTitle);
|
||||
Assert.Equal(typeof(RadzenButton), resultingType);
|
||||
}
|
||||
}
|
||||
|
||||
public class OpenSideDialogTests
|
||||
{
|
||||
[Fact(DisplayName = "SideDialogOptions resizable option is retained after OpenSideDialog call")]
|
||||
public void SideDialogOptions_Resizable_AreRetained_AfterOpenSideDialogCall()
|
||||
{
|
||||
// Arrange
|
||||
var options = new SideDialogOptions { Resizable = true };
|
||||
SideDialogOptions resultingOptions = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnSideOpen += (_, _, sideOptions) => resultingOptions = sideOptions;
|
||||
|
||||
// Act
|
||||
dialogService.OpenSide<DialogServiceTests>("Test", [], options);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Same(options, resultingOptions);
|
||||
Assert.True(resultingOptions.Resizable);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Side dialog shows resize bar when Resizable is true")]
|
||||
public void SideDialog_Resizable_ShowsResizeBar()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.Services.AddScoped<DialogService>();
|
||||
|
||||
// Render the dialog host
|
||||
var cut = ctx.RenderComponent<RadzenDialog>();
|
||||
|
||||
// Open a side dialog with Resizable=true
|
||||
var dialogService = ctx.Services.GetRequiredService<DialogService>();
|
||||
cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton),
|
||||
new Dictionary<string, object>(), new SideDialogOptions { Resizable = true }));
|
||||
|
||||
// Assert: the resize bar element is present
|
||||
cut.WaitForAssertion(() =>
|
||||
{
|
||||
var markup = cut.Markup;
|
||||
Assert.Contains("rz-dialog-resize-bar", markup);
|
||||
// Optionally ensure the inner handle exists too
|
||||
Assert.Contains("rz-resize", markup);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Side dialog hides resize bar when Resizable is false")]
|
||||
public void SideDialog_NonResizable_HidesResizeBar()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.Services.AddScoped<DialogService>();
|
||||
|
||||
// Render the dialog host
|
||||
var cut = ctx.RenderComponent<RadzenDialog>();
|
||||
|
||||
// Open a side dialog with Resizable=false
|
||||
var dialogService = ctx.Services.GetRequiredService<DialogService>();
|
||||
cut.InvokeAsync(() => dialogService.OpenSide("Test", typeof(RadzenButton),
|
||||
new Dictionary<string, object>(), new SideDialogOptions()));
|
||||
|
||||
// Assert: the resize bar element is not present
|
||||
cut.WaitForAssertion(() =>
|
||||
{
|
||||
var markup = cut.Markup;
|
||||
Assert.DoesNotContain("rz-dialog-resize-bar", markup);
|
||||
});
|
||||
}
|
||||
}
|
||||
public class ConfirmTests
|
||||
{
|
||||
[Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
@@ -50,13 +51,14 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_WithCustomTextProperty()
|
||||
public void DropDownDataGrid_Renders_WithCustomTextValueProperties()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<Customer>
|
||||
{
|
||||
new Customer { Id = 1, CompanyName = "Acme Corp" }
|
||||
new Customer { Id = 1, CompanyName = "Acme Corp", ContactName = "John Doe" },
|
||||
new Customer { Id = 2, CompanyName = "Tech Inc", ContactName = "Jane Smith" }
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<int>>(parameters =>
|
||||
@@ -67,6 +69,23 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
Assert.Contains("rz-lookup-panel", component.Markup);
|
||||
Assert.Contains("rz-data-grid", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_DataGrid()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
// DropDownDataGrid embeds a DataGrid
|
||||
Assert.Contains("rz-data-grid", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -83,6 +102,7 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
Assert.Contains("rz-lookup-search", component.Markup);
|
||||
Assert.Contains("rz-lookup-search-input", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -100,7 +120,7 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_AllowClear()
|
||||
public void DropDownDataGrid_Renders_AllowClear_WithValue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
@@ -116,6 +136,23 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains("rz-dropdown-clear-icon", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_DoesNotRender_AllowClear_WhenNotAllowed()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.AllowClear, false);
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.Value, "Item1");
|
||||
});
|
||||
|
||||
Assert.DoesNotContain("rz-dropdown-clear-icon", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_Disabled()
|
||||
{
|
||||
@@ -131,18 +168,122 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_Multiple_WithChips()
|
||||
public void DropDownDataGrid_Renders_Multiple_Panel()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<IEnumerable<int>>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Chips, true);
|
||||
});
|
||||
|
||||
Assert.Contains("rz-multiselect-panel", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_Multiple_WithChips()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1", "Item2" };
|
||||
var selectedItems = new List<string> { "Item1" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<IEnumerable<string>>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Chips, true);
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.Value, selectedItems);
|
||||
});
|
||||
|
||||
Assert.Contains("rz-dropdown-chips-wrapper", component.Markup);
|
||||
Assert.Contains("rz-chip", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_AllowSorting()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<Customer>
|
||||
{
|
||||
new Customer { Id = 1, CompanyName = "Acme" }
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<int>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.AllowSorting, true);
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.TextProperty, "CompanyName");
|
||||
});
|
||||
|
||||
Assert.Contains("rz-data-grid", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_SearchTextPlaceholder()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.AllowFiltering, true);
|
||||
parameters.Add(p => p.SearchTextPlaceholder, "Type to filter...");
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
Assert.Contains("placeholder=\"Type to filter...\"", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_EmptyText()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, new List<string>());
|
||||
parameters.Add(p => p.EmptyText, "No items found");
|
||||
});
|
||||
|
||||
Assert.Contains("No items found", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_PageSize()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = Enumerable.Range(1, 20).Select(i => $"Item {i}").ToList();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
parameters.Add(p => p.PageSize, 10);
|
||||
});
|
||||
|
||||
// DataGrid with paging should be present
|
||||
Assert.Contains("rz-data-grid", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropDownDataGrid_Renders_AllowRowSelectOnRowClick()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1", "Item2" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenDropDownDataGrid<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.AllowRowSelectOnRowClick, true);
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
Assert.Contains("rz-data-grid", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
@@ -1026,6 +1026,36 @@ public class ExpressionParserTests
|
||||
Assert.True(func(new Person { BirthDate = DateTime.Parse("5/5/2000 12:00:00 AM") }));
|
||||
}
|
||||
|
||||
class EmployeeWithHireDate
|
||||
{
|
||||
public DateTime? HireDate { get; set; }
|
||||
public DateOnly? HireDateOnly { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SupportNullableDateTimeArrayWithSpecifyKindAndNullableProperty()
|
||||
{
|
||||
var predicate = "x => new System.DateTime?[] { DateTime.SpecifyKind(DateTime.Parse(\"2012-04-01\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.Unspecified) }.Contains((x.HireDate ?? null))";
|
||||
var expression = ExpressionParser.ParsePredicate<EmployeeWithHireDate>(predicate);
|
||||
var func = expression.Compile();
|
||||
|
||||
var hireDate = DateTime.SpecifyKind(DateTime.Parse("2012-04-01", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind), DateTimeKind.Unspecified);
|
||||
Assert.True(func(new EmployeeWithHireDate { HireDate = hireDate }));
|
||||
Assert.False(func(new EmployeeWithHireDate { HireDate = DateTime.Parse("2013-01-01") }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SupportNullableDateOnlyArrayAndNullableProperty()
|
||||
{
|
||||
var predicate = "x => new System.DateOnly?[] { DateOnly.Parse(\"2012-04-01\") }.Contains((x.HireDateOnly ?? null))";
|
||||
var expression = ExpressionParser.ParsePredicate<EmployeeWithHireDate>(predicate);
|
||||
var func = expression.Compile();
|
||||
|
||||
var hireDate = DateOnly.Parse("2012-04-01");
|
||||
Assert.True(func(new EmployeeWithHireDate { HireDateOnly = hireDate }));
|
||||
Assert.False(func(new EmployeeWithHireDate { HireDateOnly = DateOnly.Parse("2013-01-01") }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_SupportNumericConversion()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -22,7 +22,7 @@ namespace Radzen.Blazor.Tests
|
||||
public Guid Id { get; set; }
|
||||
public TimeOnly StartTime { get; set; }
|
||||
public DateOnly BirthDate { get; set; }
|
||||
public int[] Scores { get; set; }
|
||||
public IEnumerable<int> Scores { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public List<TestEntity> Children { get; set; }
|
||||
public Address Address { get; set; }
|
||||
@@ -319,5 +319,35 @@ namespace Radzen.Blazor.Tests
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("Member");
|
||||
Assert.Equal("e => (!(e.Tags.Contains(\"Member\")))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DefaultExpression_ReferenceType()
|
||||
{
|
||||
// Simulates the NullPropagate pattern: x.Address == null ? default(string) : x.Address.City
|
||||
var param = Expression.Parameter(typeof(TestEntity), "x");
|
||||
var address = Expression.Property(param, "Address");
|
||||
var city = Expression.Property(address, "City");
|
||||
var isNull = Expression.Equal(address, Expression.Constant(null, typeof(Address)));
|
||||
var whenNull = Expression.Default(typeof(string));
|
||||
var conditional = Expression.Condition(isNull, whenNull, city);
|
||||
var lambda = Expression.Lambda<Func<TestEntity, string>>(conditional, param);
|
||||
|
||||
Assert.Equal("x => ((x.Address == null) ? null : x.Address.City)", _serializer.Serialize(lambda));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DefaultExpression_ValueType()
|
||||
{
|
||||
// Simulates a conditional with a default value type
|
||||
var param = Expression.Parameter(typeof(TestEntity), "x");
|
||||
var address = Expression.Property(param, "Address");
|
||||
var age = Expression.Property(param, "Age");
|
||||
var isNull = Expression.Equal(address, Expression.Constant(null, typeof(Address)));
|
||||
var whenNull = Expression.Default(typeof(int));
|
||||
var conditional = Expression.Condition(isNull, whenNull, age);
|
||||
var lambda = Expression.Lambda<Func<TestEntity, int>>(conditional, param);
|
||||
|
||||
Assert.Equal("x => ((x.Address == null) ? default(int) : x.Age)", _serializer.Serialize(lambda));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Collapse, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find("legend button").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Expand, args => { raised = true; }));
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find("legend button").Click();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -22,22 +22,6 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains(@"rz-listbox", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_ListWrapper()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Apple", "Banana", "Cherry" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenListBox<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
Assert.Contains("rz-listbox-list-wrapper", component.Markup);
|
||||
Assert.Contains("rz-listbox-list", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_WithData_SimpleList()
|
||||
{
|
||||
@@ -135,6 +119,79 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("Select an item", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_Multiple_WithCheckboxes()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1", "Item2" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenListBox<IEnumerable<string>>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
// Multiple selection shows checkboxes in header
|
||||
Assert.Contains("rz-listbox-header-w-checkbox", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_ReadOnly()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var component = ctx.RenderComponent<RadzenListBox<int>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.ReadOnly, true);
|
||||
});
|
||||
|
||||
Assert.Contains("readonly", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_TabIndex()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var component = ctx.RenderComponent<RadzenListBox<int>>();
|
||||
|
||||
Assert.Contains("tabindex=", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_ListWrapper()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var data = new List<string> { "Item1" };
|
||||
|
||||
var component = ctx.RenderComponent<RadzenListBox<string>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Data, data);
|
||||
});
|
||||
|
||||
Assert.Contains("rz-listbox-list-wrapper", component.Markup);
|
||||
Assert.Contains("rz-listbox-list", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListBox_Renders_SearchAriaLabel()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
var component = ctx.RenderComponent<RadzenListBox<int>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.AllowFiltering, true);
|
||||
parameters.Add(p => p.SearchAriaLabel, "Search items");
|
||||
parameters.Add(p => p.Data, new List<string> { "Item1" });
|
||||
});
|
||||
|
||||
Assert.Contains("aria-label=\"Search items\"", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.AllowResetPassword, true);
|
||||
});
|
||||
|
||||
Assert.Contains(@$"Forgot password?</a>", component.Markup);
|
||||
Assert.Contains(@$"Forgot password?</span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -134,7 +134,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.ResetPasswordText, "Test");
|
||||
});
|
||||
|
||||
Assert.Contains(@$"Test</a>", component.Markup);
|
||||
Assert.Contains(@$"Test</span>", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -195,7 +195,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Register, args => { clicked = true; });
|
||||
});
|
||||
|
||||
component.Find(".rz-secondary").Click();
|
||||
component.Find(".rz-secondary.rz-variant-flat").Click();
|
||||
|
||||
Assert.True(clicked);
|
||||
}
|
||||
@@ -215,7 +215,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.ResetPassword, args => { clicked = true; });
|
||||
});
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find(".rz-secondary.rz-variant-text").Click();
|
||||
|
||||
Assert.True(clicked);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.ResetPassword, args => { clicked = true; });
|
||||
});
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find(".rz-secondary.rz-variant-text").Click();
|
||||
|
||||
Assert.True(clicked);
|
||||
}
|
||||
|
||||
@@ -141,4 +141,28 @@ foo <!---> foo -->
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("***foo bar***", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("___foo bar___", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BoldAndItalic_ShouldNotThrowException(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Bunit;
|
||||
using Bunit.JSInterop;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
@@ -54,7 +55,7 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RadzenPager_Renders_Summary() {
|
||||
public async Task RadzenPager_Renders_Summary() {
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
|
||||
@@ -64,7 +65,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<int>(p => p.Count, 100);
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, true);
|
||||
});
|
||||
await component.Instance.GoToPage(2);
|
||||
await component.InvokeAsync(() => component.Instance.GoToPage(2));
|
||||
component.Render();
|
||||
|
||||
Assert.Contains(@$"rz-pager-summary", component.Markup);
|
||||
@@ -111,7 +112,7 @@ namespace Radzen.Blazor.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RadzenPager_First_And_Prev_Buttons_Are_Disabled_When_On_The_First_Page()
|
||||
public async Task RadzenPager_First_And_Prev_Buttons_Are_Disabled_When_On_The_First_Page()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
@@ -123,18 +124,18 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, true);
|
||||
});
|
||||
|
||||
await component.Instance.GoToPage(0);
|
||||
await component.InvokeAsync(() => component.Instance.GoToPage(0));
|
||||
component.Render();
|
||||
|
||||
var firstPageButton = component.Find("a.rz-pager-first");
|
||||
var firstPageButton = component.Find("button.rz-pager-first");
|
||||
Assert.True(firstPageButton.HasAttribute("disabled"));
|
||||
|
||||
var prevPageButton = component.Find("a.rz-pager-prev");
|
||||
var prevPageButton = component.Find("button.rz-pager-prev");
|
||||
Assert.True(prevPageButton.HasAttribute("disabled"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RadzenPager_Last_And_Next_Buttons_Are_Disabled_When_On_The_Last_Page()
|
||||
public async Task RadzenPager_Last_And_Next_Buttons_Are_Disabled_When_On_The_Last_Page()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
@@ -146,13 +147,13 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add<bool>(p => p.ShowPagingSummary, true);
|
||||
});
|
||||
|
||||
await component.Instance.GoToPage(9);
|
||||
await component.InvokeAsync(() => component.Instance.GoToPage(9));
|
||||
component.Render();
|
||||
|
||||
var lastPageButton = component.Find("a.rz-pager-last");
|
||||
var lastPageButton = component.Find("button.rz-pager-last");
|
||||
Assert.True(lastPageButton.HasAttribute("disabled"));
|
||||
|
||||
var nextPageButton = component.Find("a.rz-pager-next");
|
||||
var nextPageButton = component.Find("button.rz-pager-next");
|
||||
Assert.True(nextPageButton.HasAttribute("disabled"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace Radzen.Blazor.Tests
|
||||
parameters.Add(p => p.Collapse, args => { raised = true; });
|
||||
});
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find("button.rz-panel-titlebar-toggler").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Expand, args => { raised = true; }));
|
||||
|
||||
component.Find("a").Click();
|
||||
component.Find("button.rz-panel-titlebar-toggler").Click();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -171,24 +171,49 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains(@$"autofocus", component.Markup);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Password_Raises_ChangedEvent()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPassword>();
|
||||
|
||||
var raised = false;
|
||||
var hasRaised = false;
|
||||
var value = "Test";
|
||||
object newValue = null;
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));
|
||||
var component = ctx.RenderComponent<RadzenPassword>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Change, args => { hasRaised = true; newValue = args; });
|
||||
parameters.Add(p => p.Immediate, false);
|
||||
});
|
||||
|
||||
component.Find("input").Change(value);
|
||||
var inputElement = component.Find("input");
|
||||
inputElement.Change(value);
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(value, newValue));
|
||||
Assert.DoesNotContain("oninput", inputElement.ToMarkup());
|
||||
Assert.True(hasRaised);
|
||||
Assert.Equal(value, newValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Password_Raises_InputEvent()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var hasRaised = false;
|
||||
var value = "Test";
|
||||
object newValue = null;
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPassword>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Change, args => { hasRaised = true; newValue = args; });
|
||||
parameters.Add(p => p.Immediate, true);
|
||||
});
|
||||
|
||||
var inputElement = component.Find("input");
|
||||
inputElement.Input(value);
|
||||
|
||||
Assert.DoesNotContain("onchange", inputElement.ToMarkup());
|
||||
Assert.True(hasRaised);
|
||||
Assert.Equal(value, newValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
@@ -156,6 +157,64 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("disabled", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PickList_GetSelectedSources_Respects_ValueProperty_Single()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var data = new List<Item>
|
||||
{
|
||||
new Item { Id = 1, Name = "A" },
|
||||
new Item { Id = 2, Name = "B" }
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPickList<Item>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Source, data);
|
||||
parameters.Add(p => p.TextProperty, "Name");
|
||||
parameters.Add(p => p.ValueProperty, "Id");
|
||||
parameters.Add(p => p.Multiple, false);
|
||||
});
|
||||
|
||||
// Simulate ListBox selection when ValueProperty is set: selectedSourceItems becomes the value (Id)
|
||||
var field = typeof(RadzenPickList<Item>).GetField("selectedSourceItems", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
field.SetValue(component.Instance, 2);
|
||||
|
||||
var selected = component.Instance.GetSelectedSources();
|
||||
Assert.Single(selected);
|
||||
Assert.Equal(2, selected.First().Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PickList_GetSelectedSources_Respects_ValueProperty_Multiple()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
|
||||
var data = new List<Item>
|
||||
{
|
||||
new Item { Id = 1, Name = "A" },
|
||||
new Item { Id = 2, Name = "B" },
|
||||
new Item { Id = 3, Name = "C" }
|
||||
};
|
||||
|
||||
var component = ctx.RenderComponent<RadzenPickList<Item>>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Source, data);
|
||||
parameters.Add(p => p.TextProperty, "Name");
|
||||
parameters.Add(p => p.ValueProperty, "Id");
|
||||
parameters.Add(p => p.Multiple, true);
|
||||
});
|
||||
|
||||
// Simulate ListBox selection when ValueProperty is set: selectedSourceItems becomes IEnumerable of values (Ids)
|
||||
var field = typeof(RadzenPickList<Item>).GetField("selectedSourceItems", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
field.SetValue(component.Instance, new[] { 1, 3 });
|
||||
|
||||
var selected = component.Instance.GetSelectedSources().Select(i => i.Id).OrderBy(i => i).ToArray();
|
||||
Assert.Equal(new[] { 1, 3 }, selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.True(grid.AllowFieldsPicking);
|
||||
}
|
||||
|
||||
private static IRenderedComponent<RadzenPivotDataGrid<SalesData>> RenderPivotDataGrid(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenPivotDataGrid<SalesData>>>? configure = null)
|
||||
private static IRenderedComponent<RadzenPivotDataGrid<SalesData>> RenderPivotDataGrid(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenPivotDataGrid<SalesData>>> configure = null)
|
||||
{
|
||||
return ctx.RenderComponent<RadzenPivotDataGrid<SalesData>>(parameters =>
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
@@ -114,6 +114,11 @@ namespace Radzen.Blazor.Tests
|
||||
public List<string> Values { get; set; }
|
||||
}
|
||||
|
||||
public class Order
|
||||
{
|
||||
public DateTime? OrderDate { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProperty_Should_Resolve_DescriptionProperty()
|
||||
{
|
||||
@@ -137,6 +142,14 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.NotNull(idProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPropertyType_Resolves_NullableDateTime_Date()
|
||||
{
|
||||
var propertyType = PropertyAccess.GetPropertyType(typeof(Order), "OrderDate.Date");
|
||||
|
||||
Assert.Equal(typeof(DateTime), propertyType);
|
||||
}
|
||||
|
||||
interface ISimpleInterface : ISimpleNestedInterface
|
||||
{
|
||||
string Description { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using Radzen;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
@@ -24,6 +25,11 @@ namespace Radzen.Blazor.Tests
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
private class Order
|
||||
{
|
||||
public DateTime? OrderDate { get; set; }
|
||||
}
|
||||
|
||||
private List<TestItem> GetTestData()
|
||||
{
|
||||
return new List<TestItem>
|
||||
@@ -36,6 +42,24 @@ namespace Radzen.Blazor.Tests
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNestedPropertyExpression_Handles_NullableDateTime_Date()
|
||||
{
|
||||
var parameter = Expression.Parameter(typeof(Order), "x");
|
||||
var expression = QueryableExtension.GetNestedPropertyExpression(parameter, "OrderDate.Date");
|
||||
var getter = Expression.Lambda<Func<Order, DateTime>>(expression, parameter).Compile();
|
||||
|
||||
var order = new Order { OrderDate = new DateTime(2024, 2, 3, 14, 30, 0) };
|
||||
var result = getter(order);
|
||||
|
||||
Assert.Equal(order.OrderDate.Value.Date, result);
|
||||
|
||||
var nullOrder = new Order { OrderDate = null };
|
||||
var nullResult = getter(nullOrder);
|
||||
|
||||
Assert.Equal(default(DateTime), nullResult);
|
||||
}
|
||||
|
||||
// OrderBy tests
|
||||
[Fact]
|
||||
public void OrderBy_SortsAscending_ByDefault()
|
||||
@@ -1689,6 +1713,838 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Id);
|
||||
}
|
||||
|
||||
// CollectionFilterMode tests
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag5", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "tag1",
|
||||
FilterOperator = FilterOperator.Equals,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has Name == "tag1"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "active", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "active", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "active", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "active",
|
||||
FilterOperator = FilterOperator.Equals,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have Name == "active"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithGreaterThan()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 60 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 8 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 50,
|
||||
FilterOperator = FilterOperator.GreaterThan,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has Value > 50
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithGreaterThan()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 60 }, new { Name = "tag2", Value = 70 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 60 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 55 }, new { Name = "tag6", Value = 80 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 50,
|
||||
FilterOperator = FilterOperator.GreaterThan,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have Value > 50
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithContains()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "important-task", Value = 10 }, new { Name = "review", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "normal", Value = 30 }, new { Name = "basic", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "important-meeting", Value = 50 }, new { Name = "urgent", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "important",
|
||||
FilterOperator = FilterOperator.Contains,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag contains "important"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithContains()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "important-task", Value = 10 }, new { Name = "important-note", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "important-meeting", Value = 30 }, new { Name = "basic", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "important-reminder", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "important",
|
||||
FilterOperator = FilterOperator.Contains,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags contain "important"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithStartsWith()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "prefix_alpha", Value = 10 }, new { Name = "other_beta", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "prefix_gamma", Value = 30 }, new { Name = "prefix_delta", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "other_epsilon", Value = 50 }, new { Name = "other_zeta", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "prefix_",
|
||||
FilterOperator = FilterOperator.StartsWith,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag starts with "prefix_"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithStartsWith()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "prefix_alpha", Value = 10 }, new { Name = "prefix_beta", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "prefix_gamma", Value = 30 }, new { Name = "other_delta", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "prefix_epsilon", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "prefix_",
|
||||
FilterOperator = FilterOperator.StartsWith,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags start with "prefix_"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithEndsWith()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "file.txt", Value = 10 }, new { Name = "doc.pdf", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "image.png", Value = 30 }, new { Name = "photo.txt", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "data.csv", Value = 50 }, new { Name = "info.doc", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = ".txt",
|
||||
FilterOperator = FilterOperator.EndsWith,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag ends with ".txt"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithEndsWith()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "file.txt", Value = 10 }, new { Name = "doc.txt", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "image.txt", Value = 30 }, new { Name = "photo.png", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "data.txt", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = ".txt",
|
||||
FilterOperator = FilterOperator.EndsWith,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags end with ".txt"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithNotEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "inactive", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "inactive", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "pending", Value = 50 }, new { Name = "active", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "inactive",
|
||||
FilterOperator = FilterOperator.NotEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag is not equal to "inactive"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithNotEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "pending", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "active", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "pending", Value = 50 }, new { Name = "active", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "inactive",
|
||||
FilterOperator = FilterOperator.NotEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags are not equal to "inactive"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithDoesNotContain()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "test-alpha", Value = 10 }, new { Name = "beta", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "test-gamma", Value = 30 }, new { Name = "test-delta", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "epsilon", Value = 50 }, new { Name = "zeta", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "test",
|
||||
FilterOperator = FilterOperator.DoesNotContain,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag does not contain "test"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithDoesNotContain()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "alpha", Value = 10 }, new { Name = "beta", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "test-gamma", Value = 30 }, new { Name = "delta", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "epsilon", Value = 50 }, new { Name = "zeta", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterValue = "test",
|
||||
FilterOperator = FilterOperator.DoesNotContain,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags do not contain "test"
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithLessThan()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 50 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 60 }, new { Name = "tag4", Value = 70 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 15 }, new { Name = "tag6", Value = 80 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 20,
|
||||
FilterOperator = FilterOperator.LessThan,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has Value < 20
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithLessThan()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 15 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 18 }, new { Name = "tag4", Value = 70 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 12 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 20,
|
||||
FilterOperator = FilterOperator.LessThan,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have Value < 20
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithLessThanOrEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 20 }, new { Name = "tag2", Value = 50 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 60 }, new { Name = "tag4", Value = 70 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 15 }, new { Name = "tag6", Value = 80 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 20,
|
||||
FilterOperator = FilterOperator.LessThanOrEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has Value <= 20
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithLessThanOrEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 18 }, new { Name = "tag4", Value = 70 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 12 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 20,
|
||||
FilterOperator = FilterOperator.LessThanOrEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have Value <= 20
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithGreaterThanOrEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 10 }, new { Name = "tag4", Value = 15 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 60 }, new { Name = "tag6", Value = 30 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 50,
|
||||
FilterOperator = FilterOperator.GreaterThanOrEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has Value >= 50
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithGreaterThanOrEquals()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag2", Value = 60 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 55 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 70 }, new { Name = "tag6", Value = 80 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Value",
|
||||
FilterValue = 50,
|
||||
FilterOperator = FilterOperator.GreaterThanOrEquals,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have Value >= 50
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNull()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = (string)null, Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNull,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has null Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNull()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = (string)null, Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = (string)null, Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = (string)null, Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNull,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have null Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNotNull()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = (string)null, Value = 30 }, new { Name = (string)null, Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNotNull,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has non-null Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNotNull()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = (string)null, Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNotNull,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have non-null Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsEmpty()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsEmpty,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has empty Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsEmpty()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsEmpty,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have empty Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNotEmpty()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "", Value = 30 }, new { Name = "", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNotEmpty,
|
||||
CollectionFilterMode = CollectionFilterMode.Any
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where at least one tag has non-empty Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNotEmpty()
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() },
|
||||
new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "", Value = 40 } }.ToList() },
|
||||
new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 } }.ToList() }
|
||||
}.AsQueryable();
|
||||
|
||||
var filters = new List<FilterDescriptor>
|
||||
{
|
||||
new FilterDescriptor
|
||||
{
|
||||
Property = "Tags",
|
||||
FilterProperty = "Name",
|
||||
FilterOperator = FilterOperator.IsNotEmpty,
|
||||
CollectionFilterMode = CollectionFilterMode.All
|
||||
}
|
||||
};
|
||||
|
||||
var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList();
|
||||
|
||||
// Should return items where all tags have non-empty Name
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, r => r.Id == 1);
|
||||
Assert.Contains(result, r => r.Id == 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9</TargetFramework>
|
||||
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageReference Include="bunit.web" Version="1.36.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
|
||||
63
Radzen.Blazor.Tests/SchedulerYearRangeTests.cs
Normal file
63
Radzen.Blazor.Tests/SchedulerYearRangeTests.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Bunit;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Radzen.Blazor;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using Radzen;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class SchedulerYearRangeTests
|
||||
{
|
||||
class Appointment
|
||||
{
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime End { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YearView_StartMonthJanuary_IncludesLastDaysOfYear_WhenYearStartsOnFirstDayOfWeek()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
ctx.Services.AddScoped<DialogService>();
|
||||
ctx.JSInterop.Setup<Rect>("Radzen.createScheduler", _ => true)
|
||||
.SetResult(new Rect { Left = 0, Top = 0, Width = 200, Height = 200 });
|
||||
|
||||
// Make the first day of week Monday and use a year where Jan 1 is Monday (2024-01-01).
|
||||
var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone();
|
||||
culture.DateTimeFormat.FirstDayOfWeek = DayOfWeek.Monday;
|
||||
|
||||
var appointments = new List<Appointment>
|
||||
{
|
||||
new() { Start = new DateTime(2024, 12, 31), End = new DateTime(2025, 1, 1), Text = "Year end" }
|
||||
};
|
||||
|
||||
var cut = ctx.RenderComponent<RadzenScheduler<Appointment>>(p =>
|
||||
{
|
||||
p.Add(x => x.Culture, culture);
|
||||
p.Add(x => x.Date, new DateTime(2024, 6, 1));
|
||||
p.Add(x => x.Data, appointments);
|
||||
p.Add(x => x.StartProperty, nameof(Appointment.Start));
|
||||
p.Add(x => x.EndProperty, nameof(Appointment.End));
|
||||
p.Add(x => x.TextProperty, nameof(Appointment.Text));
|
||||
p.AddChildContent<RadzenYearView>(v => v.Add(x => x.StartMonth, Radzen.Month.January));
|
||||
});
|
||||
|
||||
var view = Assert.IsType<RadzenYearView>(cut.Instance.SelectedView);
|
||||
|
||||
// View should start on 2023-12-25 (one extra week above since 2024-01-01 is Monday).
|
||||
Assert.Equal(new DateTime(2023, 12, 25), view.StartDate.Date);
|
||||
|
||||
// View end must include 2024-12-31 (it should extend to end-of-week containing the real year end).
|
||||
Assert.True(view.EndDate.Date >= new DateTime(2024, 12, 31), $"EndDate was {view.EndDate:yyyy-MM-dd}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));
|
||||
|
||||
component.Find("div").Click();
|
||||
component.Find("input[type=\"checkbox\"]").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(value, !(bool)newValue));
|
||||
@@ -129,7 +129,7 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; }));
|
||||
|
||||
component.Find("div").Click();
|
||||
component.Find("input[type=\"checkbox\"]").Click();
|
||||
|
||||
Assert.True(raised);
|
||||
Assert.True(object.Equals(value, !(bool)newValue));
|
||||
|
||||
27
Radzen.Blazor.Tests/TocTests.cs
Normal file
27
Radzen.Blazor.Tests/TocTests.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Bunit;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class TocTests
|
||||
{
|
||||
[Fact]
|
||||
public void TocItem_Renders_With_Attributes()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenTocItem>(parameters =>
|
||||
{
|
||||
parameters.Add(p => p.Attributes, new Dictionary<string, object>
|
||||
{
|
||||
{ "data-enhance-nav", "false" },
|
||||
{ "aria-label", "Table of Contents Item" }
|
||||
});
|
||||
});
|
||||
|
||||
Assert.Contains("data-enhance-nav=\"false\"", component.Markup);
|
||||
Assert.Contains("aria-label=\"Table of Contents Item\"", component.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,48 @@ root = true
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
dotnet_diagnostic.CA1002.severity = none
|
||||
dotnet_diagnostic.CA1003.severity = none
|
||||
dotnet_diagnostic.CA1024.severity = none
|
||||
dotnet_diagnostic.CA1030.severity = none
|
||||
dotnet_diagnostic.CA1031.severity = none
|
||||
dotnet_diagnostic.CA1033.severity = none
|
||||
dotnet_diagnostic.CA1044.severity = none
|
||||
dotnet_diagnostic.CA1050.severity = none
|
||||
dotnet_diagnostic.CA1051.severity = none
|
||||
dotnet_diagnostic.CA1052.severity = none
|
||||
dotnet_diagnostic.CA1054.severity = none
|
||||
dotnet_diagnostic.CA1055.severity = none
|
||||
dotnet_diagnostic.CA1056.severity = none
|
||||
dotnet_diagnostic.CA1063.severity = none
|
||||
dotnet_diagnostic.CA1068.severity = none
|
||||
dotnet_diagnostic.CA1308.severity = none
|
||||
dotnet_diagnostic.CA1708.severity = none
|
||||
dotnet_diagnostic.CA1711.severity = none
|
||||
dotnet_diagnostic.CA1716.severity = none
|
||||
dotnet_diagnostic.CA1720.severity = none
|
||||
dotnet_diagnostic.CA1721.severity = none
|
||||
dotnet_diagnostic.CA1724.severity = none
|
||||
dotnet_diagnostic.CA1725.severity = none
|
||||
dotnet_diagnostic.CA1802.severity = none
|
||||
dotnet_diagnostic.CA1814.severity = none
|
||||
dotnet_diagnostic.CA1815.severity = none
|
||||
dotnet_diagnostic.CA1816.severity = none
|
||||
dotnet_diagnostic.CA1819.severity = none
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
dotnet_diagnostic.CA1827.severity = none
|
||||
dotnet_diagnostic.CA1834.severity = none
|
||||
dotnet_diagnostic.CA1845.severity = none
|
||||
dotnet_diagnostic.CA1849.severity = none
|
||||
dotnet_diagnostic.CA1851.severity = none
|
||||
dotnet_diagnostic.CA1859.severity = none
|
||||
dotnet_diagnostic.CA1863.severity = none
|
||||
dotnet_diagnostic.CA1869.severity = none
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
dotnet_diagnostic.CA2012.severity = none
|
||||
dotnet_diagnostic.CA2211.severity = none
|
||||
dotnet_diagnostic.CA2227.severity = none
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
@@ -10,206 +10,19 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a conversation session with memory.
|
||||
/// </summary>
|
||||
public class ConversationSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the conversation session.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of messages in the conversation.
|
||||
/// </summary>
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the conversation and manages memory limits.
|
||||
/// </summary>
|
||||
/// <param name="role">The role of the message sender.</param>
|
||||
/// <param name="content">The message content.</param>
|
||||
public void AddMessage(string role, string content)
|
||||
{
|
||||
Messages.Add(new ChatMessage
|
||||
{
|
||||
UserId = role,
|
||||
IsUser = role != "system",
|
||||
Content = content,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
|
||||
LastUpdated = DateTime.Now;
|
||||
|
||||
// Remove oldest messages if we exceed the limit
|
||||
while (Messages.Count > MaxMessages)
|
||||
{
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages from the conversation.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Messages.Clear();
|
||||
LastUpdated = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation messages formatted for the AI API.
|
||||
/// </summary>
|
||||
/// <param name="systemPrompt">The system prompt to include.</param>
|
||||
/// <returns>A list of message objects for the AI API.</returns>
|
||||
public List<object> GetFormattedMessages(string systemPrompt)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
|
||||
// Add system message
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
|
||||
// Add conversation messages
|
||||
foreach (var message in Messages)
|
||||
{
|
||||
messages.Add(new { role = message.IsUser ? "user" : "system", content = message.Content });
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for getting chat completions from an AI model with conversation memory.
|
||||
/// </summary>
|
||||
public interface IAIChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams chat completion responses from the AI model asynchronously with conversation memory.
|
||||
/// </summary>
|
||||
/// <param name="userInput">The user's input message to send to the AI model.</param>
|
||||
/// <param name="sessionId">Optional session ID to maintain conversation context. If null, a new session will be created.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <param name="model">Optional model name to override the configured model.</param>
|
||||
/// <param name="systemPrompt">Optional system prompt to override the configured system prompt.</param>
|
||||
/// <param name="temperature">Optional temperature to override the configured temperature.</param>
|
||||
/// <param name="maxTokens">Optional maximum tokens to override the configured max tokens.</param>
|
||||
/// <param name="endpoint">Optional endpoint URL to override the configured endpoint.</param>
|
||||
/// <param name="proxy">Optional proxy URL to override the configured proxy.</param>
|
||||
/// <param name="apiKey">Optional API key to override the configured API key.</param>
|
||||
/// <param name="apiKeyHeader">Optional API key header name to override the configured header.</param>
|
||||
/// <returns>An async enumerable that yields streaming response chunks from the AI model.</returns>
|
||||
IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null, string endpoint = null, string proxy = null, string apiKey = null, string apiKeyHeader = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a conversation session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID. If null, a new session will be created.</param>
|
||||
/// <returns>The conversation session.</returns>
|
||||
ConversationSession GetOrCreateSession(string sessionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the conversation history for a specific session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID to clear.</param>
|
||||
void ClearSession(string sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active conversation sessions.
|
||||
/// </summary>
|
||||
/// <returns>A list of active conversation sessions.</returns>
|
||||
IEnumerable<ConversationSession> GetActiveSessions();
|
||||
|
||||
/// <summary>
|
||||
/// Removes old conversation sessions based on age.
|
||||
/// </summary>
|
||||
/// <param name="maxAgeHours">Maximum age in hours for sessions to keep.</param>
|
||||
void CleanupOldSessions(int maxAgeHours = 24);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the <see cref="AIChatService"/>.
|
||||
/// </summary>
|
||||
public class AIChatServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint URL for the AI service.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy URL for the AI service, if any. If set, this will override the Endpoint.
|
||||
/// </summary>
|
||||
public string Proxy { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for authentication with the AI service.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header name for the API key (e.g., 'Authorization' or 'api-key').
|
||||
/// </summary>
|
||||
public string ApiKeyHeader { get; set; } = "Authorization";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model name to use for executing chat completions (e.g., 'gpt-3.5-turbo').
|
||||
/// </summary>
|
||||
public string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system prompt for the AI assistant.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = "You are a helpful AI code assistant.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the temperature for the AI model (0.0 to 2.0). Set to 0.0 for deterministic responses, higher values for more creative outputs.
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of tokens to generate in the response.
|
||||
/// </summary>
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in conversation memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age in hours for conversation sessions before cleanup.
|
||||
/// </summary>
|
||||
public int SessionMaxAgeHours { get; set; } = 24;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with AI chat models to get completions with conversation memory.
|
||||
/// </summary>
|
||||
public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServiceOptions> options) : IAIChatService
|
||||
{
|
||||
private readonly Dictionary<string, ConversationSession> _sessions = new();
|
||||
private readonly object _sessionsLock = new();
|
||||
private readonly Dictionary<string, ConversationSession> sessions = new();
|
||||
private readonly object sessionsLock = new();
|
||||
|
||||
// Add this static field to cache the JsonSerializerOptions instance
|
||||
private static readonly JsonSerializerOptions CachedJsonSerializerOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration options for the chat streaming service.
|
||||
@@ -217,7 +30,7 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
public AIChatServiceOptions Options => options.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null, string endpoint = null, string proxy = null, string apiKey = null, string apiKeyHeader = null)
|
||||
public async IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string? sessionId = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default, string? model = null, string? systemPrompt = null, double? temperature = null, int? maxTokens = null, string? endpoint = null, string? proxy = null, string? apiKey = null, string? apiKeyHeader = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
{
|
||||
@@ -226,7 +39,7 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
|
||||
// Get or create session
|
||||
var session = GetOrCreateSession(sessionId);
|
||||
|
||||
|
||||
// Add user message to conversation history
|
||||
session.AddMessage("user", userInput);
|
||||
|
||||
@@ -247,13 +60,18 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
stream = true
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(JsonSerializer.Serialize(payload, CachedJsonSerializerOptions), Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(effectiveApiKey))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(effectiveApiKeyHeader))
|
||||
{
|
||||
throw new InvalidOperationException("API key header must be specified when an API key is provided.");
|
||||
}
|
||||
|
||||
if (string.Equals(effectiveApiKeyHeader, "Authorization", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", effectiveApiKey);
|
||||
@@ -265,23 +83,22 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
}
|
||||
|
||||
var httpClient = serviceProvider.GetRequiredService<HttpClient>();
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Chat stream failed: {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||
throw new HttpRequestException($"Chat stream failed: {await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)}");
|
||||
}
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var assistantResponse = new StringBuilder();
|
||||
|
||||
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) is not null && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data:"))
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data:", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -309,23 +126,23 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConversationSession GetOrCreateSession(string sessionId = null)
|
||||
public ConversationSession GetOrCreateSession(string? sessionId = null)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
lock (sessionsLock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
sessionId = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
if (!sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session = new ConversationSession
|
||||
{
|
||||
Id = sessionId,
|
||||
MaxMessages = Options.MaxMessages
|
||||
};
|
||||
_sessions[sessionId] = session;
|
||||
sessions[sessionId] = session;
|
||||
}
|
||||
|
||||
return session;
|
||||
@@ -335,9 +152,9 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
/// <inheritdoc />
|
||||
public void ClearSession(string sessionId)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
lock (sessionsLock)
|
||||
{
|
||||
if (_sessions.TryGetValue(sessionId, out var session))
|
||||
if (sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session.Clear();
|
||||
}
|
||||
@@ -347,35 +164,40 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ConversationSession> GetActiveSessions()
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
lock (sessionsLock)
|
||||
{
|
||||
return _sessions.Values.ToList();
|
||||
return sessions.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CleanupOldSessions(int maxAgeHours = 24)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
lock (sessionsLock)
|
||||
{
|
||||
var cutoffTime = DateTime.Now.AddHours(-maxAgeHours);
|
||||
var sessionsToRemove = _sessions.Values
|
||||
var sessionsToRemove = sessions.Values
|
||||
.Where(s => s.LastUpdated < cutoffTime)
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
foreach (var sessionId in sessionsToRemove)
|
||||
{
|
||||
_sessions.Remove(sessionId);
|
||||
sessions.Remove(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseStreamingResponse(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(json);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
|
||||
@@ -397,52 +219,13 @@ public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServ
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch
|
||||
catch (JsonException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring AIChatService in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class AIChatServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with the specified configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">The action to configure the AIChatService options.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services, Action<AIChatServiceOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with default options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<AIChatServiceOptions>();
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
41
Radzen.Blazor/AIChatServiceExtensions.cs
Normal file
41
Radzen.Blazor/AIChatServiceExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring AIChatService in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class AIChatServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with the specified configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">The action to configure the AIChatService options.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services, Action<AIChatServiceOptions> configureOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configureOptions);
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with default options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<AIChatServiceOptions>();
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
58
Radzen.Blazor/AIChatServiceOptions.cs
Normal file
58
Radzen.Blazor/AIChatServiceOptions.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the <see cref="AIChatService"/>.
|
||||
/// </summary>
|
||||
public class AIChatServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint URL for the AI service.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy URL for the AI service, if any. If set, this will override the Endpoint.
|
||||
/// </summary>
|
||||
public string? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for authentication with the AI service.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header name for the API key (e.g., 'Authorization' or 'api-key').
|
||||
/// </summary>
|
||||
public string ApiKeyHeader { get; set; } = "Authorization";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model name to use for executing chat completions (e.g., 'gpt-3.5-turbo').
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system prompt for the AI assistant.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = "You are a helpful AI code assistant.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the temperature for the AI model (0.0 to 2.0). Set to 0.0 for deterministic responses, higher values for more creative outputs.
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of tokens to generate in the response.
|
||||
/// </summary>
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in conversation memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age in hours for conversation sessions before cleanup.
|
||||
/// </summary>
|
||||
public int SessionMaxAgeHours { get; set; } = 24;
|
||||
}
|
||||
|
||||
43
Radzen.Blazor/AggregateFunction.cs
Normal file
43
Radzen.Blazor/AggregateFunction.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the aggregate function for pivot values.
|
||||
/// </summary>
|
||||
public enum AggregateFunction
|
||||
{
|
||||
/// <summary>
|
||||
/// Sum of values.
|
||||
/// </summary>
|
||||
Sum,
|
||||
|
||||
/// <summary>
|
||||
/// Count of items.
|
||||
/// </summary>
|
||||
Count,
|
||||
|
||||
/// <summary>
|
||||
/// Average of values.
|
||||
/// </summary>
|
||||
Average,
|
||||
|
||||
/// <summary>
|
||||
/// Minimum value.
|
||||
/// </summary>
|
||||
Min,
|
||||
|
||||
/// <summary>
|
||||
/// Maximum value.
|
||||
/// </summary>
|
||||
Max,
|
||||
|
||||
/// <summary>
|
||||
/// First value.
|
||||
/// </summary>
|
||||
First,
|
||||
|
||||
/// <summary>
|
||||
/// Last value.
|
||||
/// </summary>
|
||||
Last
|
||||
}
|
||||
|
||||
28
Radzen.Blazor/AlertSize.cs
Normal file
28
Radzen.Blazor/AlertSize.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the size of a <see cref="Radzen.Blazor.RadzenAlert" />.
|
||||
/// </summary>
|
||||
public enum AlertSize
|
||||
{
|
||||
/// <summary>
|
||||
/// The smallest alert.
|
||||
/// </summary>
|
||||
ExtraSmall,
|
||||
|
||||
/// <summary>
|
||||
/// A alert smaller than the default.
|
||||
/// </summary>
|
||||
Small,
|
||||
|
||||
/// <summary>
|
||||
/// The default size of an alert.
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// An alert larger than the default.
|
||||
/// </summary>
|
||||
Large
|
||||
}
|
||||
|
||||
53
Radzen.Blazor/AlertStyle.cs
Normal file
53
Radzen.Blazor/AlertStyle.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the display style or severity of a <see cref="Radzen.Blazor.RadzenAlert" />. Affects the visual styling of RadzenAlert (background and text color).
|
||||
/// </summary>
|
||||
public enum AlertStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// Primary styling. Similar to primary buttons.
|
||||
/// </summary>
|
||||
Primary,
|
||||
|
||||
/// <summary>
|
||||
/// Secondary styling. Similar to secondary buttons.
|
||||
/// </summary>
|
||||
Secondary,
|
||||
|
||||
/// <summary>
|
||||
/// Light styling. Similar to light buttons.
|
||||
/// </summary>
|
||||
Light,
|
||||
|
||||
/// <summary>
|
||||
/// Base styling. Similar to base buttons.
|
||||
/// </summary>
|
||||
Base,
|
||||
|
||||
/// <summary>
|
||||
/// Dark styling. Similar to dark buttons.
|
||||
/// </summary>
|
||||
Dark,
|
||||
|
||||
/// <summary>
|
||||
/// Success styling.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Danger styling.
|
||||
/// </summary>
|
||||
Danger,
|
||||
|
||||
/// <summary>
|
||||
/// Warning styling.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Informative styling.
|
||||
/// </summary>
|
||||
Info
|
||||
}
|
||||
|
||||
33
Radzen.Blazor/AlignItems.cs
Normal file
33
Radzen.Blazor/AlignItems.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the alignment of Stack items.
|
||||
/// </summary>
|
||||
public enum AlignItems
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal items alignment.
|
||||
/// </summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>
|
||||
/// Center items alignment.
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// Start items alignment.
|
||||
/// </summary>
|
||||
Start,
|
||||
|
||||
/// <summary>
|
||||
/// End items alignment.
|
||||
/// </summary>
|
||||
End,
|
||||
|
||||
/// <summary>
|
||||
/// Stretch items alignment.
|
||||
/// </summary>
|
||||
Stretch
|
||||
}
|
||||
|
||||
@@ -22,19 +22,19 @@ namespace Radzen.Blazor
|
||||
/// Gets or sets the text of the appointment.
|
||||
/// </summary>
|
||||
/// <value>The text.</value>
|
||||
public string Text { get; set; }
|
||||
public string? Text { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the data associated with the appointment
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
public object Data { get; set; }
|
||||
public object? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to this instance. Used to check if two appointments are equal.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with this instance.</param>
|
||||
/// <returns><c>true</c> if the specified is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is AppointmentData data &&
|
||||
Start == data.Start &&
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <value>The stroke.</value>
|
||||
[Parameter]
|
||||
public string Stroke { get; set; }
|
||||
public string? Stroke { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the pixel width of axis.
|
||||
/// </summary>
|
||||
@@ -26,21 +26,21 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <value>The child content.</value>
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format string used to display the axis values.
|
||||
/// </summary>
|
||||
/// <value>The format string.</value>
|
||||
[Parameter]
|
||||
public string FormatString { get; set; }
|
||||
public string? FormatString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a formatter function that formats the axis values.
|
||||
/// </summary>
|
||||
/// <value>The formatter.</value>
|
||||
[Parameter]
|
||||
public Func<object, string> Formatter { get; set; }
|
||||
public Func<object, string>? Formatter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the line used to display the axis.
|
||||
@@ -80,20 +80,20 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <value>The minimum.</value>
|
||||
[Parameter]
|
||||
public object Min { get; set; }
|
||||
public object? Min { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the maximum value of the axis.
|
||||
/// </summary>
|
||||
/// <value>The maximum.</value>
|
||||
[Parameter]
|
||||
public object Max { get; set; }
|
||||
public object? Max { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the step of the axis.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public object Step { get; set; }
|
||||
public object? Step { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="AxisBase"/> is visible.
|
||||
@@ -139,7 +139,7 @@ namespace Radzen.Blazor
|
||||
}
|
||||
else
|
||||
{
|
||||
return scale.FormatTick(FormatString, value);
|
||||
return scale.FormatTick(FormatString ?? string.Empty, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
Radzen.Blazor/BadgeStyle.cs
Normal file
53
Radzen.Blazor/BadgeStyle.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the display style of a <see cref="Radzen.Blazor.RadzenBadge" />. Affects the visual styling of RadzenBadge (background and text color).
|
||||
/// </summary>
|
||||
public enum BadgeStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// Primary styling. Similar to primary buttons.
|
||||
/// </summary>
|
||||
Primary,
|
||||
|
||||
/// <summary>
|
||||
/// Secondary styling. Similar to secondary buttons.
|
||||
/// </summary>
|
||||
Secondary,
|
||||
|
||||
/// <summary>
|
||||
/// Light styling. Similar to light buttons.
|
||||
/// </summary>
|
||||
Light,
|
||||
|
||||
/// <summary>
|
||||
/// Base styling. Similar to base buttons.
|
||||
/// </summary>
|
||||
Base,
|
||||
|
||||
/// <summary>
|
||||
/// Dark styling. Similar to dark buttons.
|
||||
/// </summary>
|
||||
Dark,
|
||||
|
||||
/// <summary>
|
||||
/// Success styling.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Danger styling.
|
||||
/// </summary>
|
||||
Danger,
|
||||
|
||||
/// <summary>
|
||||
/// Warning styling.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Informative styling.
|
||||
/// </summary>
|
||||
Info
|
||||
}
|
||||
|
||||
28
Radzen.Blazor/ButtonSize.cs
Normal file
28
Radzen.Blazor/ButtonSize.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the size of a <see cref="Radzen.Blazor.RadzenButton" />.
|
||||
/// </summary>
|
||||
public enum ButtonSize
|
||||
{
|
||||
/// <summary>
|
||||
/// The default size of a button.
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// A button larger than the default.
|
||||
/// </summary>
|
||||
Large,
|
||||
|
||||
/// <summary>
|
||||
/// A button smaller than the default.
|
||||
/// </summary>
|
||||
Small,
|
||||
|
||||
/// <summary>
|
||||
/// The smallest button.
|
||||
/// </summary>
|
||||
ExtraSmall
|
||||
}
|
||||
|
||||
53
Radzen.Blazor/ButtonStyle.cs
Normal file
53
Radzen.Blazor/ButtonStyle.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the display style of a <see cref="Radzen.Blazor.RadzenButton" />. Affects the visual styling of RadzenButton (background and text color).
|
||||
/// </summary>
|
||||
public enum ButtonStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// A primary button. Clicking it performs the primary action in a form or dialog (e.g. "save").
|
||||
/// </summary>
|
||||
Primary,
|
||||
|
||||
/// <summary>
|
||||
/// A secondary button. Clicking it performs a secondary action in a form or dialog (e.g. close a dialog or cancel a form).
|
||||
/// </summary>
|
||||
Secondary,
|
||||
|
||||
/// <summary>
|
||||
/// A button with lighter styling.
|
||||
/// </summary>
|
||||
Light,
|
||||
|
||||
/// <summary>
|
||||
/// The base UI styling.
|
||||
/// </summary>
|
||||
Base,
|
||||
|
||||
/// <summary>
|
||||
/// A button with dark styling.
|
||||
/// </summary>
|
||||
Dark,
|
||||
|
||||
/// <summary>
|
||||
/// A button with success styling.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// A button which represents a dangerous action e.g. "delete".
|
||||
/// </summary>
|
||||
Danger,
|
||||
|
||||
/// <summary>
|
||||
/// A button with warning styling.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// A button with informative styling.
|
||||
/// </summary>
|
||||
Info
|
||||
}
|
||||
|
||||
23
Radzen.Blazor/ButtonType.cs
Normal file
23
Radzen.Blazor/ButtonType.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of a <see cref="Radzen.Blazor.RadzenButton" />. Renders as the <c>type</c> HTML attribute.
|
||||
/// </summary>
|
||||
public enum ButtonType
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic button which does not submit its parent form.
|
||||
/// </summary>
|
||||
Button,
|
||||
|
||||
/// <summary>
|
||||
/// Clicking a submit button submits its parent form.
|
||||
/// </summary>
|
||||
Submit,
|
||||
|
||||
/// <summary>
|
||||
/// Clicking a reset button clears the value of all inputs in its parent form.
|
||||
/// </summary>
|
||||
Reset
|
||||
}
|
||||
|
||||
@@ -19,7 +19,17 @@ namespace Radzen.Blazor
|
||||
/// Cache for the value returned by <see cref="Category"/> when that value is only dependent on
|
||||
/// <see cref="CategoryProperty"/>.
|
||||
/// </summary>
|
||||
Func<TItem, double> categoryPropertyCache;
|
||||
Func<TItem, double>? categoryPropertyCache;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the parent <see cref="RadzenChart"/> instance or throws an <see cref="InvalidOperationException"/> if not present.
|
||||
/// </summary>
|
||||
/// <returns>The parent <see cref="RadzenChart"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the parent chart is not set.</exception>
|
||||
protected RadzenChart RequireChart()
|
||||
{
|
||||
return Chart ?? throw new InvalidOperationException($"{GetType().Name} requires a parent RadzenChart.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a getter function that returns a value from the specified category scale for the specified data item.
|
||||
@@ -34,13 +44,13 @@ namespace Radzen.Blazor
|
||||
|
||||
if (IsNumeric(CategoryProperty))
|
||||
{
|
||||
categoryPropertyCache = PropertyAccess.Getter<TItem, double>(CategoryProperty);
|
||||
categoryPropertyCache = PropertyAccess.Getter<TItem, double>(CategoryProperty!);
|
||||
return categoryPropertyCache;
|
||||
}
|
||||
|
||||
if (IsDate(CategoryProperty))
|
||||
{
|
||||
var category = PropertyAccess.Getter<TItem, DateTime>(CategoryProperty);
|
||||
var category = PropertyAccess.Getter<TItem, DateTime>(CategoryProperty!);
|
||||
categoryPropertyCache = (item) => category(item).Ticks;
|
||||
return categoryPropertyCache;
|
||||
}
|
||||
@@ -49,7 +59,7 @@ namespace Radzen.Blazor
|
||||
{
|
||||
Func<TItem, object> category = String.IsNullOrEmpty(CategoryProperty) ? (item) => string.Empty : PropertyAccess.Getter<TItem, object>(CategoryProperty);
|
||||
|
||||
return (item) => ordinal.Data.IndexOf(category(item));
|
||||
return (item) => ordinal.Data?.IndexOf(category(item)) ?? -1;
|
||||
}
|
||||
|
||||
return (item) => Items.IndexOf(item);
|
||||
@@ -60,6 +70,8 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
protected Func<TItem, double> ComposeCategory(ScaleBase scale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scale);
|
||||
|
||||
return scale.Compose(Category(scale));
|
||||
}
|
||||
|
||||
@@ -68,6 +80,8 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
protected Func<TItem, double> ComposeValue(ScaleBase scale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scale);
|
||||
|
||||
return scale.Compose(Value);
|
||||
}
|
||||
|
||||
@@ -77,7 +91,7 @@ namespace Radzen.Blazor
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
/// <returns><c>true</c> if the specified property name is date; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
|
||||
protected bool IsDate(string propertyName)
|
||||
protected bool IsDate(string? propertyName)
|
||||
{
|
||||
if (String.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
@@ -91,12 +105,10 @@ namespace Radzen.Blazor
|
||||
throw new ArgumentException($"Property {propertyName} does not exist");
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
if(PropertyAccess.IsDateOnly(property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return PropertyAccess.IsDate(property);
|
||||
}
|
||||
|
||||
@@ -106,7 +118,7 @@ namespace Radzen.Blazor
|
||||
/// <param name="propertyName">Name of the property.</param>
|
||||
/// <returns><c>true</c> if the specified property name is numeric; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
|
||||
protected bool IsNumeric(string propertyName)
|
||||
protected bool IsNumeric(string? propertyName)
|
||||
{
|
||||
if (String.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
@@ -125,21 +137,21 @@ namespace Radzen.Blazor
|
||||
|
||||
/// <inheritdoc />
|
||||
[Parameter]
|
||||
public string Title { get; set; }
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the child content.
|
||||
/// </summary>
|
||||
/// <value>The child content.</value>
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tooltip template.
|
||||
/// </summary>
|
||||
/// <value>The tooltip template.</value>
|
||||
[Parameter]
|
||||
public RenderFragment<TItem> TooltipTemplate { get; set; }
|
||||
public RenderFragment<TItem>? TooltipTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of overlays.
|
||||
@@ -157,7 +169,7 @@ namespace Radzen.Blazor
|
||||
/// The name of the property of <typeparamref name="TItem" /> that provides the X axis (a.k.a. category axis) values.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string CategoryProperty { get; set; }
|
||||
public string? CategoryProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="CartesianSeries{TItem}"/> is visible.
|
||||
@@ -194,7 +206,7 @@ namespace Radzen.Blazor
|
||||
/// The name of the property of <typeparamref name="TItem" /> that provides the Y axis (a.k.a. value axis) values.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ValueProperty { get; set; }
|
||||
public string? ValueProperty { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Parameter]
|
||||
@@ -223,7 +235,7 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
[Parameter]
|
||||
public IEnumerable<TItem> Data { get; set; }
|
||||
public IEnumerable<TItem>? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores <see cref="Data" /> as an IList of <typeparamref name="TItem"/>.
|
||||
@@ -272,6 +284,8 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public virtual ScaleBase TransformCategoryScale(ScaleBase scale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scale);
|
||||
|
||||
if (Items == null)
|
||||
{
|
||||
return scale;
|
||||
@@ -298,7 +312,7 @@ namespace Radzen.Blazor
|
||||
|
||||
var data = GetCategories();
|
||||
|
||||
if (scale is OrdinalScale ordinal)
|
||||
if (scale is OrdinalScale ordinal && ordinal.Data != null)
|
||||
{
|
||||
foreach (var item in ordinal.Data)
|
||||
{
|
||||
@@ -328,6 +342,8 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public virtual ScaleBase TransformValueScale(ScaleBase scale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scale);
|
||||
|
||||
if (Items != null)
|
||||
{
|
||||
if (Items.Any())
|
||||
@@ -398,29 +414,32 @@ namespace Radzen.Blazor
|
||||
{
|
||||
if (Data != null)
|
||||
{
|
||||
if (Data is IList<TItem>)
|
||||
if (Data is IList<TItem> list)
|
||||
{
|
||||
Items = Data as IList<TItem>;
|
||||
Items = list;
|
||||
}
|
||||
else
|
||||
{
|
||||
Items = Data.ToList();
|
||||
}
|
||||
|
||||
if (IsDate(CategoryProperty) || IsNumeric(CategoryProperty))
|
||||
if (!string.IsNullOrEmpty(CategoryProperty) && (IsDate(CategoryProperty) || IsNumeric(CategoryProperty)))
|
||||
{
|
||||
Items = Items.AsQueryable().OrderBy(CategoryProperty).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
await Chart.Refresh(false);
|
||||
if (Chart != null)
|
||||
{
|
||||
await Chart.Refresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Initialize()
|
||||
{
|
||||
Chart.AddSeries(this);
|
||||
Chart?.AddSeries(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -443,6 +462,9 @@ namespace Radzen.Blazor
|
||||
/// <returns><c>true</c> if the polygon contains the point, <c>false</c> otherwise.</returns>
|
||||
protected bool InsidePolygon(Point point, Point[] polygon)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(point);
|
||||
ArgumentNullException.ThrowIfNull(polygon);
|
||||
|
||||
var minX = polygon[0].X;
|
||||
var maxX = polygon[0].X;
|
||||
var minY = polygon[0].Y;
|
||||
@@ -479,18 +501,22 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public virtual RenderFragment RenderTooltip(object data)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var item = (TItem)data;
|
||||
|
||||
|
||||
return builder =>
|
||||
{
|
||||
if (Chart.Tooltip.Shared)
|
||||
if (chart.Tooltip.Shared)
|
||||
{
|
||||
var category = PropertyAccess.GetValue(item, CategoryProperty);
|
||||
builder.OpenComponent<ChartSharedTooltip>(0);
|
||||
builder.AddAttribute(1, nameof(ChartSharedTooltip.Class), TooltipClass(item));
|
||||
builder.AddAttribute(2, nameof(ChartSharedTooltip.Title), TooltipTitle(item));
|
||||
builder.AddAttribute(3, nameof(ChartSharedTooltip.ChildContent), RenderSharedTooltipItems(category));
|
||||
builder.CloseComponent();
|
||||
var category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(item, CategoryProperty) : null;
|
||||
if (category != null)
|
||||
{
|
||||
builder.OpenComponent<ChartSharedTooltip>(0);
|
||||
builder.AddAttribute(1, nameof(ChartSharedTooltip.Class), TooltipClass(item));
|
||||
builder.AddAttribute(2, nameof(ChartSharedTooltip.Title), TooltipTitle(item));
|
||||
builder.AddAttribute(3, nameof(ChartSharedTooltip.ChildContent), RenderSharedTooltipItems(category));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -508,9 +534,11 @@ namespace Radzen.Blazor
|
||||
|
||||
private RenderFragment RenderSharedTooltipItems(object category)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
|
||||
return builder =>
|
||||
{
|
||||
var visibleSeries = Chart.Series.Where(s => s.Visible).ToList();
|
||||
var visibleSeries = chart.Series.Where(s => s.Visible).ToList();
|
||||
|
||||
foreach (var series in visibleSeries)
|
||||
{
|
||||
@@ -524,7 +552,7 @@ namespace Radzen.Blazor
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
var item = Items.FirstOrDefault(i => object.Equals(PropertyAccess.GetValue(i, CategoryProperty), category));
|
||||
var item = Items.FirstOrDefault(i => !string.IsNullOrEmpty(CategoryProperty) && object.Equals(PropertyAccess.GetValue(i, CategoryProperty), category));
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
@@ -553,7 +581,7 @@ namespace Radzen.Blazor
|
||||
/// <param name="item">The item.</param>
|
||||
protected virtual string TooltipStyle(TItem item)
|
||||
{
|
||||
return Chart.Tooltip.Style;
|
||||
return Chart?.Tooltip?.Style ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -562,7 +590,13 @@ namespace Radzen.Blazor
|
||||
/// <param name="item">The item.</param>
|
||||
protected virtual string TooltipClass(TItem item)
|
||||
{
|
||||
return $"rz-series-{Chart.Series.IndexOf(this)}-tooltip";
|
||||
var chart = Chart;
|
||||
if (chart == null)
|
||||
{
|
||||
return "rz-series-tooltip";
|
||||
}
|
||||
|
||||
return $"rz-series-{chart.Series.IndexOf(this)}-tooltip";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -576,6 +610,8 @@ namespace Radzen.Blazor
|
||||
/// </summary>
|
||||
protected virtual RenderFragment RenderLegendItem(bool clickable)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var index = chart.Series.IndexOf(this);
|
||||
var style = new List<string>();
|
||||
|
||||
if (IsVisible == false)
|
||||
@@ -586,7 +622,7 @@ namespace Radzen.Blazor
|
||||
return builder =>
|
||||
{
|
||||
builder.OpenComponent<LegendItem>(0);
|
||||
builder.AddAttribute(1, nameof(LegendItem.Index), Chart.Series.IndexOf(this));
|
||||
builder.AddAttribute(1, nameof(LegendItem.Index), index);
|
||||
builder.AddAttribute(2, nameof(LegendItem.Color), Color);
|
||||
builder.AddAttribute(3, nameof(LegendItem.MarkerType), MarkerType);
|
||||
builder.AddAttribute(4, nameof(LegendItem.Style), string.Join(";", style));
|
||||
@@ -617,19 +653,35 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public double GetMedian()
|
||||
{
|
||||
return Data.Select(e => Value(e)).OrderBy(e => e).Skip(Data.Count() / 2).FirstOrDefault();
|
||||
var values = Items.Select(Value).OrderBy(e => e).ToList();
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return values[values.Count / 2];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetMean()
|
||||
{
|
||||
return Data.Select(e => Value(e)).DefaultIfEmpty(double.NaN).Average();
|
||||
return Items.Any() ? Items.Select(Value).Average() : double.NaN;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetMode()
|
||||
{
|
||||
return Data.Any() ? Data.GroupBy(e => Value(e)).Select(g => new { Value = g.Key, Count = g.Count() }).OrderByDescending(e => e.Count).FirstOrDefault().Value : double.NaN;
|
||||
if (!Items.Any())
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
return Items
|
||||
.GroupBy(item => Value(item))
|
||||
.Select(g => new { Value = g.Key, Count = g.Count() })
|
||||
.OrderByDescending(e => e.Count)
|
||||
.First()
|
||||
.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -639,33 +691,43 @@ namespace Radzen.Blazor
|
||||
{
|
||||
double a = double.NaN, b = double.NaN;
|
||||
|
||||
if (Data.Any())
|
||||
var chart = Chart;
|
||||
if (chart == null)
|
||||
{
|
||||
return (a, b);
|
||||
}
|
||||
|
||||
if (Items.Any())
|
||||
{
|
||||
Func<TItem, double> X;
|
||||
Func<TItem, double> Y;
|
||||
if (Chart.ShouldInvertAxes())
|
||||
if (chart.ShouldInvertAxes())
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Value(e));
|
||||
Y = e => Chart.ValueScale.Scale(Category(Chart.ValueScale)(e));
|
||||
var valueScale = chart.ValueScale;
|
||||
var categoryAccessor = Category(chart.ValueScale);
|
||||
X = e => chart.CategoryScale.Scale(Value(e));
|
||||
Y = e => valueScale.Scale(categoryAccessor(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
X = e => Chart.CategoryScale.Scale(Category(Chart.CategoryScale)(e));
|
||||
Y = e => Chart.ValueScale.Scale(Value(e));
|
||||
var categoryAccessor = Category(chart.CategoryScale);
|
||||
X = e => chart.CategoryScale.Scale(categoryAccessor(e));
|
||||
Y = e => chart.ValueScale.Scale(Value(e));
|
||||
}
|
||||
|
||||
var avgX = Data.Select(e => X(e)).Average();
|
||||
var avgY = Data.Select(e => Y(e)).Average();
|
||||
var sumXY = Data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
|
||||
if (Chart.ShouldInvertAxes())
|
||||
var data = Items.ToList();
|
||||
var avgX = data.Select(e => X(e)).Average();
|
||||
var avgY = data.Select(e => Y(e)).Average();
|
||||
var sumXY = data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
|
||||
if (chart.ShouldInvertAxes())
|
||||
{
|
||||
var sumYSq = Data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
|
||||
var sumYSq = data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
|
||||
b = sumXY / sumYSq;
|
||||
a = avgX - b * avgY;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sumXSq = Data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
|
||||
var sumXSq = data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
|
||||
b = sumXY / sumXSq;
|
||||
a = avgY - b * avgX;
|
||||
}
|
||||
@@ -678,7 +740,9 @@ namespace Radzen.Blazor
|
||||
{
|
||||
IsVisible = !IsVisible;
|
||||
|
||||
if (Chart.LegendClick.HasDelegate)
|
||||
var chart = Chart;
|
||||
|
||||
if (chart?.LegendClick.HasDelegate == true)
|
||||
{
|
||||
var args = new LegendClickEventArgs
|
||||
{
|
||||
@@ -687,18 +751,28 @@ namespace Radzen.Blazor
|
||||
IsVisible = IsVisible,
|
||||
};
|
||||
|
||||
await Chart.LegendClick.InvokeAsync(args);
|
||||
await chart.LegendClick.InvokeAsync(args);
|
||||
|
||||
IsVisible = args.IsVisible;
|
||||
}
|
||||
|
||||
await Chart.Refresh();
|
||||
if (chart != null)
|
||||
{
|
||||
await chart.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetTitle()
|
||||
{
|
||||
return String.IsNullOrEmpty(Title) ? $"Series {Chart.Series.IndexOf(this) + 1}" : Title;
|
||||
var chart = Chart;
|
||||
if (string.IsNullOrEmpty(Title))
|
||||
{
|
||||
var index = chart?.Series.IndexOf(this) ?? 0;
|
||||
return $"Series {index + 1}";
|
||||
}
|
||||
|
||||
return Title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -716,8 +790,9 @@ namespace Radzen.Blazor
|
||||
/// <param name="item">The item.</param>
|
||||
protected virtual string TooltipTitle(TItem item)
|
||||
{
|
||||
var category = Category(Chart.CategoryScale);
|
||||
return Chart.CategoryAxis.Format(Chart.CategoryScale, Chart.CategoryScale.Value(category(item)));
|
||||
var chart = RequireChart();
|
||||
var category = Category(chart.CategoryScale);
|
||||
return chart.CategoryAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(category(item)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -727,7 +802,8 @@ namespace Radzen.Blazor
|
||||
/// <returns>System.String.</returns>
|
||||
protected virtual string TooltipValue(TItem item)
|
||||
{
|
||||
return Chart.ValueAxis.Format(Chart.ValueScale, Chart.ValueScale.Value(Value(item)));
|
||||
var chart = RequireChart();
|
||||
return chart.ValueAxis.Format(chart.ValueScale, chart.ValueScale.Value(Value(item)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -736,8 +812,9 @@ namespace Radzen.Blazor
|
||||
/// <param name="item">The item.</param>
|
||||
internal virtual double TooltipX(TItem item)
|
||||
{
|
||||
var category = Category(Chart.CategoryScale);
|
||||
return Chart.CategoryScale.Scale(category(item), true);
|
||||
var chart = RequireChart();
|
||||
var category = Category(chart.CategoryScale);
|
||||
return chart.CategoryScale.Scale(category(item), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -746,7 +823,8 @@ namespace Radzen.Blazor
|
||||
/// <param name="item">The item.</param>
|
||||
internal virtual double TooltipY(TItem item)
|
||||
{
|
||||
return Chart.ValueScale.Scale(Value(item), true);
|
||||
var chart = RequireChart();
|
||||
return chart.ValueScale.Scale(Value(item), true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -760,25 +838,26 @@ namespace Radzen.Blazor
|
||||
return new { Item = item, Distance = distance };
|
||||
}).Aggregate((a, b) => a.Distance < b.Distance ? a : b).Item;
|
||||
|
||||
return (retObject,
|
||||
return (retObject!,
|
||||
new Point() { X = TooltipX(retObject), Y = TooltipY(retObject)});
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
return (default!, new Point());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var list = new List<ChartDataLabel>();
|
||||
|
||||
foreach (var d in Data)
|
||||
foreach (var d in Items)
|
||||
{
|
||||
list.Add(new ChartDataLabel
|
||||
{
|
||||
Position = new Point { X = TooltipX(d) + offsetX, Y = TooltipY(d) + offsetY },
|
||||
TextAnchor = "middle",
|
||||
Text = Chart.ValueAxis.Format(Chart.ValueScale, Value(d))
|
||||
Text = chart.ValueAxis.Format(chart.ValueScale, Value(d))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -793,12 +872,12 @@ namespace Radzen.Blazor
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
/// <param name="colorRange">The color range value.</param>
|
||||
/// <param name="value">The value of the item.</param>
|
||||
protected string PickColor(int index, IEnumerable<string> colors, string defaultValue = null, IList<SeriesColorRange> colorRange = null, double value = 0.0)
|
||||
protected string? PickColor(int index, IEnumerable<string>? colors, string? defaultValue = null, IList<SeriesColorRange>? colorRange = null, double value = 0.0)
|
||||
{
|
||||
if (colorRange != null)
|
||||
{
|
||||
var result = colorRange.Where(r => r.Min <= value && r.Max >= value).FirstOrDefault<SeriesColorRange>();
|
||||
return result != null ? result.Color : defaultValue;
|
||||
return result?.Color ?? defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -819,18 +898,20 @@ namespace Radzen.Blazor
|
||||
/// <inheritdoc />
|
||||
public async Task InvokeClick(EventCallback<SeriesClickEventArgs> handler, object data)
|
||||
{
|
||||
var category = Category(Chart.CategoryScale);
|
||||
var chart = RequireChart();
|
||||
var category = Category(chart.CategoryScale);
|
||||
var dataItem = (TItem)data;
|
||||
|
||||
await handler.InvokeAsync(new SeriesClickEventArgs
|
||||
{
|
||||
Data = data,
|
||||
Title = GetTitle(),
|
||||
Category = PropertyAccess.GetValue(data, CategoryProperty),
|
||||
Value = PropertyAccess.GetValue(data, ValueProperty),
|
||||
Category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(data, CategoryProperty) : null,
|
||||
Value = !string.IsNullOrEmpty(ValueProperty) ? PropertyAccess.GetValue(data, ValueProperty) : null,
|
||||
Point = new SeriesPoint
|
||||
{
|
||||
Category = category((TItem)data),
|
||||
Value = Value((TItem)data)
|
||||
Category = category(dataItem),
|
||||
Value = Value(dataItem)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,4 +36,8 @@ public class ChatMessage
|
||||
/// Gets or sets whether this message is currently streaming.
|
||||
/// </summary>
|
||||
public bool IsStreaming { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the role associated with the message (e.g., "user", "assistant").
|
||||
/// </summary>
|
||||
public string? Role { get; set; }
|
||||
}
|
||||
18
Radzen.Blazor/CollectionFilterMode.cs
Normal file
18
Radzen.Blazor/CollectionFilterMode.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how the filter should be applied to a collection of items.
|
||||
/// </summary>
|
||||
public enum CollectionFilterMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The filter condition is satisfied if at least one item in the collection matches.
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// The filter condition is satisfied only if all items in the collection match.
|
||||
/// </summary>
|
||||
All
|
||||
}
|
||||
|
||||
258
Radzen.Blazor/Colors.cs
Normal file
258
Radzen.Blazor/Colors.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Colors.
|
||||
/// </summary>
|
||||
public static class Colors
|
||||
{
|
||||
/// <summary>
|
||||
/// Primary.
|
||||
/// </summary>
|
||||
public const string Primary = "var(--rz-primary)";
|
||||
|
||||
/// <summary>
|
||||
/// Primary lighter.
|
||||
/// </summary>
|
||||
public const string PrimaryLighter = "var(--rz-primary-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Primary light.
|
||||
/// </summary>
|
||||
public const string PrimaryLight = "var(--rz-primary-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Primary dark.
|
||||
/// </summary>
|
||||
public const string PrimaryDark = "var(--rz-primary-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Primary darker.
|
||||
/// </summary>
|
||||
public const string PrimaryDarker = "var(--rz-primary-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// Secondary.
|
||||
/// </summary>
|
||||
public const string Secondary = "var(--rz-secondary)";
|
||||
|
||||
/// <summary>
|
||||
/// Secondary lighter.
|
||||
/// </summary>
|
||||
public const string SecondaryLighter = "var(--rz-secondary-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Secondary light.
|
||||
/// </summary>
|
||||
public const string SecondaryLight = "var(--rz-secondary-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Secondary dark.
|
||||
/// </summary>
|
||||
public const string SecondaryDark = "var(--rz-secondary-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Secondary darker.
|
||||
/// </summary>
|
||||
public const string SecondaryDarker = "var(--rz-secondary-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// Info.
|
||||
/// </summary>
|
||||
public const string Info = "var(--rz-info)";
|
||||
|
||||
/// <summary>
|
||||
/// Info lighter.
|
||||
/// </summary>
|
||||
public const string InfoLighter = "var(--rz-info-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Info light.
|
||||
/// </summary>
|
||||
public const string InfoLight = "var(--rz-info-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Info dark.
|
||||
/// </summary>
|
||||
public const string InfoDark = "var(--rz-info-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Info darker.
|
||||
/// </summary>
|
||||
public const string InfoDarker = "var(--rz-info-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// Success.
|
||||
/// </summary>
|
||||
public const string Success = "var(--rz-success)";
|
||||
|
||||
/// <summary>
|
||||
/// Success lighter.
|
||||
/// </summary>
|
||||
public const string SuccessLighter = "var(--rz-success-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Success light.
|
||||
/// </summary>
|
||||
public const string SuccessLight = "var(--rz-success-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Success dark.
|
||||
/// </summary>
|
||||
public const string SuccessDark = "var(--rz-success-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Success darker.
|
||||
/// </summary>
|
||||
public const string SuccessDarker = "var(--rz-success-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// Warning.
|
||||
/// </summary>
|
||||
public const string Warning = "var(--rz-warning)";
|
||||
|
||||
/// <summary>
|
||||
/// Warning lighter.
|
||||
/// </summary>
|
||||
public const string WarningLighter = "var(--rz-warning-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Warning light.
|
||||
/// </summary>
|
||||
public const string WarningLight = "var(--rz-warning-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Warning dark.
|
||||
/// </summary>
|
||||
public const string WarningDark = "var(--rz-warning-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Warning darker.
|
||||
/// </summary>
|
||||
public const string WarningDarker = "var(--rz-warning-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// Danger.
|
||||
/// </summary>
|
||||
public const string Danger = "var(--rz-danger)";
|
||||
|
||||
/// <summary>
|
||||
/// Danger lighter.
|
||||
/// </summary>
|
||||
public const string DangerLighter = "var(--rz-danger-lighter)";
|
||||
|
||||
/// <summary>
|
||||
/// Danger light.
|
||||
/// </summary>
|
||||
public const string DangerLight = "var(--rz-danger-light)";
|
||||
|
||||
/// <summary>
|
||||
/// Danger dark.
|
||||
/// </summary>
|
||||
public const string DangerDark = "var(--rz-danger-dark)";
|
||||
|
||||
/// <summary>
|
||||
/// Danger darker.
|
||||
/// </summary>
|
||||
public const string DangerDarker = "var(--rz-danger-darker)";
|
||||
|
||||
/// <summary>
|
||||
/// White.
|
||||
/// </summary>
|
||||
public const string White = "var(--rz-white)";
|
||||
|
||||
/// <summary>
|
||||
/// Black.
|
||||
/// </summary>
|
||||
public const string Black = "var(--rz-black)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 50.
|
||||
/// </summary>
|
||||
public const string Base50 = "var(--rz-base-50)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 100.
|
||||
/// </summary>
|
||||
public const string Base100 = "var(--rz-base-100)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 200.
|
||||
/// </summary>
|
||||
public const string Base200 = "var(--rz-base-200)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 300.
|
||||
/// </summary>
|
||||
public const string Base300 = "var(--rz-base-300)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 400.
|
||||
/// </summary>
|
||||
public const string Base400 = "var(--rz-base-400)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 500.
|
||||
/// </summary>
|
||||
public const string Base500 = "var(--rz-base-500)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 600.
|
||||
/// </summary>
|
||||
public const string Base600 = "var(--rz-base-600)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 700.
|
||||
/// </summary>
|
||||
public const string Base700 = "var(--rz-base-700)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 800.
|
||||
/// </summary>
|
||||
public const string Base800 = "var(--rz-base-800)";
|
||||
|
||||
/// <summary>
|
||||
/// Base 900.
|
||||
/// </summary>
|
||||
public const string Base900 = "var(--rz-base-900)";
|
||||
|
||||
/// <summary>
|
||||
/// Series1.
|
||||
/// </summary>
|
||||
public const string Series1 = "var(--rz-series-1)";
|
||||
|
||||
/// <summary>
|
||||
/// Series2.
|
||||
/// </summary>
|
||||
public const string Series2 = "var(--rz-series-2)";
|
||||
|
||||
/// <summary>
|
||||
/// Series3.
|
||||
/// </summary>
|
||||
public const string Series3 = "var(--rz-series-3)";
|
||||
|
||||
/// <summary>
|
||||
/// Series4.
|
||||
/// </summary>
|
||||
public const string Series4 = "var(--rz-series-4)";
|
||||
|
||||
/// <summary>
|
||||
/// Series5.
|
||||
/// </summary>
|
||||
public const string Series5 = "var(--rz-series-5)";
|
||||
|
||||
/// <summary>
|
||||
/// Series6.
|
||||
/// </summary>
|
||||
public const string Series6 = "var(--rz-series-6)";
|
||||
|
||||
/// <summary>
|
||||
/// Series7.
|
||||
/// </summary>
|
||||
public const string Series7 = "var(--rz-series-7)";
|
||||
|
||||
/// <summary>
|
||||
/// Series8.
|
||||
/// </summary>
|
||||
public const string Series8 = "var(--rz-series-8)";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
53
Radzen.Blazor/CompositeFilterDescriptor.cs
Normal file
53
Radzen.Blazor/CompositeFilterDescriptor.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter in a component that supports filtering.
|
||||
/// </summary>
|
||||
public class CompositeFilterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string? Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the property type.
|
||||
/// </summary>
|
||||
/// <value>The property type.</value>
|
||||
public Type? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string? FilterProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to filter by.
|
||||
/// </summary>
|
||||
/// <value>The filter value.</value>
|
||||
public object? FilterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator which will compare the property value with <see cref="FilterValue" />.
|
||||
/// </summary>
|
||||
/// <value>The filter operator.</value>
|
||||
public FilterOperator? FilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logic used to combine the outcome of filtering by <see cref="FilterValue" />.
|
||||
/// </summary>
|
||||
/// <value>The logical filter operator.</value>
|
||||
public LogicalFilterOperator LogicalFilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filters.
|
||||
/// </summary>
|
||||
/// <value>The filters.</value>
|
||||
public IEnumerable<CompositeFilterDescriptor>? Filters { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,225 +1,227 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ContextMenuService. Contains various methods with options to open and close context menus.
|
||||
/// Should be added as scoped service in the application services and RadzenContextMenu should be added in application main layout.
|
||||
/// Implements the <see cref="IDisposable" />
|
||||
/// </summary>
|
||||
/// <seealso cref="IDisposable" />
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// @inject ContextMenuService ContextMenuService
|
||||
/// <RadzenButton Text="Show context menu" ContextMenu=@(args => ShowContextMenuWithContent(args)) />
|
||||
/// @code {
|
||||
/// void ShowContextMenuWithContent(MouseEventArgs args) => ContextMenuService.Open(args, ds =>
|
||||
/// @<RadzenMenu Click="OnMenuItemClick">
|
||||
/// <RadzenMenuItem Text="Item1" Value="1"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="Item2" Value="2"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="More items" Value="3">
|
||||
/// <RadzenMenuItem Text="More sub items" Value="4">
|
||||
/// <RadzenMenuItem Text="Item1" Value="5"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="Item2" Value="6"></RadzenMenuItem>
|
||||
/// </RadzenMenuItem>
|
||||
/// </RadzenMenuItem>
|
||||
/// </RadzenMenu>);
|
||||
///
|
||||
/// void OnMenuItemClick(MenuItemEventArgs args)
|
||||
/// {
|
||||
/// Console.WriteLine($"Menu item with Value={args.Value} clicked");
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ContextMenuService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the navigation manager.
|
||||
/// </summary>
|
||||
/// <value>The navigation manager.</value>
|
||||
NavigationManager navigationManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContextMenuService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uriHelper">The URI helper.</param>
|
||||
public ContextMenuService(NavigationManager uriHelper)
|
||||
{
|
||||
navigationManager = uriHelper;
|
||||
|
||||
if (navigationManager != null)
|
||||
{
|
||||
navigationManager.LocationChanged += UriHelper_OnLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the OnLocationChanged event of the UriHelper control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs"/> instance containing the event data.</param>
|
||||
private void UriHelper_OnLocationChanged(object sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ContextMenuService. Contains various methods with options to open and close context menus.
|
||||
/// Should be added as scoped service in the application services and RadzenContextMenu should be added in application main layout.
|
||||
/// Implements the <see cref="IDisposable" />
|
||||
/// </summary>
|
||||
/// <seealso cref="IDisposable" />
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// @inject ContextMenuService ContextMenuService
|
||||
/// <RadzenButton Text="Show context menu" ContextMenu=@(args => ShowContextMenuWithContent(args)) />
|
||||
/// @code {
|
||||
/// void ShowContextMenuWithContent(MouseEventArgs args) => ContextMenuService.Open(args, ds =>
|
||||
/// @<RadzenMenu Click="OnMenuItemClick">
|
||||
/// <RadzenMenuItem Text="Item1" Value="1"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="Item2" Value="2"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="More items" Value="3">
|
||||
/// <RadzenMenuItem Text="More sub items" Value="4">
|
||||
/// <RadzenMenuItem Text="Item1" Value="5"></RadzenMenuItem>
|
||||
/// <RadzenMenuItem Text="Item2" Value="6"></RadzenMenuItem>
|
||||
/// </RadzenMenuItem>
|
||||
/// </RadzenMenuItem>
|
||||
/// </RadzenMenu>);
|
||||
///
|
||||
/// void OnMenuItemClick(MenuItemEventArgs args)
|
||||
/// {
|
||||
/// Console.WriteLine($"Menu item with Value={args.Value} clicked");
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ContextMenuService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the navigation manager.
|
||||
/// </summary>
|
||||
/// <value>The navigation manager.</value>
|
||||
NavigationManager? navigationManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContextMenuService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uriHelper">The URI helper.</param>
|
||||
public ContextMenuService(NavigationManager? uriHelper)
|
||||
{
|
||||
this.OnNavigate?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [on navigate].
|
||||
/// </summary>
|
||||
public event Action OnNavigate;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the Close event.
|
||||
/// </summary>
|
||||
public event Action OnClose;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [on open].
|
||||
/// </summary>
|
||||
public event Action<MouseEventArgs, ContextMenuOptions> OnOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the specified arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="click">The click.</param>
|
||||
public void Open(MouseEventArgs args, IEnumerable<ContextMenuItem> items, Action<MenuItemEventArgs> click = null)
|
||||
{
|
||||
var options = new ContextMenuOptions();
|
||||
|
||||
options.Items = items;
|
||||
options.Click = click;
|
||||
|
||||
OpenTooltip(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the specified arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="childContent">Content of the child.</param>
|
||||
public void Open(MouseEventArgs args, RenderFragment<ContextMenuService> childContent)
|
||||
{
|
||||
var options = new ContextMenuOptions();
|
||||
|
||||
options.ChildContent = childContent;
|
||||
|
||||
OpenTooltip(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the tooltip.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void OpenTooltip(MouseEventArgs args, ContextMenuOptions options)
|
||||
{
|
||||
OnOpen?.Invoke(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this instance.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
navigationManager.LocationChanged -= UriHelper_OnLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenuOptions.
|
||||
/// </summary>
|
||||
public class ContextMenuOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the child content.
|
||||
/// </summary>
|
||||
/// <value>The child content.</value>
|
||||
public RenderFragment<ContextMenuService> ChildContent { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the items.
|
||||
/// </summary>
|
||||
/// <value>The items.</value>
|
||||
public IEnumerable<ContextMenuItem> Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the click.
|
||||
/// </summary>
|
||||
/// <value>The click.</value>
|
||||
public Action<MenuItemEventArgs> Click { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenu.
|
||||
/// </summary>
|
||||
public class ContextMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the options.
|
||||
/// </summary>
|
||||
/// <value>The options.</value>
|
||||
public ContextMenuOptions Options { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the mouse event arguments.
|
||||
/// </summary>
|
||||
/// <value>The mouse event arguments.</value>
|
||||
public MouseEventArgs MouseEventArgs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenuItem.
|
||||
/// </summary>
|
||||
public class ContextMenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
/// <value>The text.</value>
|
||||
public string Text { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
public object Value { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon.
|
||||
/// </summary>
|
||||
/// <value>The icon.</value>
|
||||
public string Icon { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon color.
|
||||
/// </summary>
|
||||
/// <value>The icon color.</value>
|
||||
public string IconColor { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image.
|
||||
/// </summary>
|
||||
/// <value>The image.</value>
|
||||
public string Image { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image style.
|
||||
/// </summary>
|
||||
/// <value>The image style.</value>
|
||||
public string ImageStyle { get; set; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
navigationManager = uriHelper;
|
||||
|
||||
if (navigationManager != null)
|
||||
{
|
||||
navigationManager.LocationChanged += UriHelper_OnLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the OnLocationChanged event of the UriHelper control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs"/> instance containing the event data.</param>
|
||||
private void UriHelper_OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
||||
{
|
||||
this.OnNavigate?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [on navigate].
|
||||
/// </summary>
|
||||
public event Action? OnNavigate;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the Close event.
|
||||
/// </summary>
|
||||
public event Action? OnClose;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [on open].
|
||||
/// </summary>
|
||||
public event Action<MouseEventArgs, ContextMenuOptions>? OnOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the specified arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="click">The click.</param>
|
||||
public void Open(MouseEventArgs args, IEnumerable<ContextMenuItem> items, Action<MenuItemEventArgs>? click = null)
|
||||
{
|
||||
var options = new ContextMenuOptions();
|
||||
|
||||
options.Items = items;
|
||||
options.Click = click;
|
||||
|
||||
OpenTooltip(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the specified arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="childContent">Content of the child.</param>
|
||||
public void Open(MouseEventArgs args, RenderFragment<ContextMenuService> childContent)
|
||||
{
|
||||
var options = new ContextMenuOptions();
|
||||
|
||||
options.ChildContent = childContent;
|
||||
|
||||
OpenTooltip(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the tooltip.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void OpenTooltip(MouseEventArgs args, ContextMenuOptions options)
|
||||
{
|
||||
OnOpen?.Invoke(args, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes this instance.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (navigationManager != null)
|
||||
{
|
||||
navigationManager.LocationChanged -= UriHelper_OnLocationChanged;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenuOptions.
|
||||
/// </summary>
|
||||
public class ContextMenuOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the child content.
|
||||
/// </summary>
|
||||
/// <value>The child content.</value>
|
||||
public RenderFragment<ContextMenuService>? ChildContent { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the items.
|
||||
/// </summary>
|
||||
/// <value>The items.</value>
|
||||
public IEnumerable<ContextMenuItem>? Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the click.
|
||||
/// </summary>
|
||||
/// <value>The click.</value>
|
||||
public Action<MenuItemEventArgs>? Click { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenu.
|
||||
/// </summary>
|
||||
public class ContextMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the options.
|
||||
/// </summary>
|
||||
/// <value>The options.</value>
|
||||
public ContextMenuOptions? Options { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the mouse event arguments.
|
||||
/// </summary>
|
||||
/// <value>The mouse event arguments.</value>
|
||||
public MouseEventArgs? MouseEventArgs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class ContextMenuItem.
|
||||
/// </summary>
|
||||
public class ContextMenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
/// <value>The text.</value>
|
||||
public string? Text { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
public object? Value { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon.
|
||||
/// </summary>
|
||||
/// <value>The icon.</value>
|
||||
public string? Icon { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the icon color.
|
||||
/// </summary>
|
||||
/// <value>The icon color.</value>
|
||||
public string? IconColor { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image.
|
||||
/// </summary>
|
||||
/// <value>The image.</value>
|
||||
public string? Image { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the image style.
|
||||
/// </summary>
|
||||
/// <value>The image style.</value>
|
||||
public string? ImageStyle { get; set; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is disabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
92
Radzen.Blazor/ConversationSession.cs
Normal file
92
Radzen.Blazor/ConversationSession.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a conversation session with memory.
|
||||
/// </summary>
|
||||
public class ConversationSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the conversation session.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of messages in the conversation.
|
||||
/// </summary>
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the conversation and manages memory limits.
|
||||
/// </summary>
|
||||
/// <param name="role">The role of the message sender.</param>
|
||||
/// <param name="content">The message content.</param>
|
||||
public void AddMessage(string role, string content)
|
||||
{
|
||||
Messages.Add(new ChatMessage
|
||||
{
|
||||
UserId = role,
|
||||
Role = role,
|
||||
IsUser = role == "user",
|
||||
Content = content,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
|
||||
LastUpdated = DateTime.Now;
|
||||
|
||||
// Remove oldest messages if we exceed the limit
|
||||
while (Messages.Count > MaxMessages)
|
||||
{
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages from the conversation.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Messages.Clear();
|
||||
LastUpdated = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation messages formatted for the AI API.
|
||||
/// </summary>
|
||||
/// <param name="systemPrompt">The system prompt to include.</param>
|
||||
/// <returns>A list of message objects for the AI API.</returns>
|
||||
public List<object> GetFormattedMessages(string systemPrompt)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
|
||||
// Add system message
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
|
||||
// Add conversation messages
|
||||
foreach (var message in Messages)
|
||||
{
|
||||
messages.Add(new { role = message.Role, content = message.Content });
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
70
Radzen.Blazor/ConvertType.cs
Normal file
70
Radzen.Blazor/ConvertType.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Converts values to different types. Used internally.
|
||||
/// </summary>
|
||||
public static class ConvertType
|
||||
{
|
||||
/// <summary>
|
||||
/// Changes the type of an object.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>System.Object</returns>
|
||||
public static object? ChangeType(object value, Type type, CultureInfo? culture = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
|
||||
// CA1062: Validate 'value' is non-null before using it
|
||||
if (value == null)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(type) != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (culture == null)
|
||||
{
|
||||
culture = CultureInfo.CurrentCulture;
|
||||
}
|
||||
|
||||
if ((Nullable.GetUnderlyingType(type) ?? type) == typeof(Guid) && value is string)
|
||||
{
|
||||
return Guid.Parse((string)value);
|
||||
}
|
||||
|
||||
var underlyingEnumType = Nullable.GetUnderlyingType(type);
|
||||
if (underlyingEnumType?.IsEnum == true)
|
||||
{
|
||||
var valueString = value.ToString();
|
||||
if (valueString == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value), "Enum value cannot be null.");
|
||||
}
|
||||
return Enum.Parse(underlyingEnumType, valueString);
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
{
|
||||
Type itemType = type.GetGenericArguments()[0];
|
||||
var enumerable = value as IEnumerable<object>;
|
||||
|
||||
if (enumerable != null)
|
||||
{
|
||||
return enumerable.AsQueryable().Cast(itemType);
|
||||
}
|
||||
}
|
||||
|
||||
return value is IConvertible ? Convert.ChangeType(value, Nullable.GetUnderlyingType(type) ?? type, culture) : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,18 +44,18 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use secure cookies.
|
||||
/// </summary>
|
||||
public bool IsSecure { get; set; } = false;
|
||||
public bool IsSecure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SameSite attribute for the cookie.
|
||||
/// </summary>
|
||||
public CookieSameSiteMode? SameSite { get; set; } = null;
|
||||
public CookieSameSiteMode? SameSite { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist the current theme in a cookie. Requires <see cref="ThemeService" /> to be registered in the DI container.
|
||||
/// </summary>
|
||||
public class CookieThemeService
|
||||
public class CookieThemeService : IDisposable
|
||||
{
|
||||
private readonly CookieThemeServiceOptions options;
|
||||
private readonly IJSRuntime jsRuntime;
|
||||
@@ -64,13 +64,16 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CookieThemeService" /> class.
|
||||
/// </summary>
|
||||
public CookieThemeService(IJSRuntime jsRuntime, ThemeService themeService, IOptions<CookieThemeServiceOptions> options)
|
||||
public CookieThemeService(IJSRuntime jsRuntime, ThemeService themeService, IOptions<CookieThemeServiceOptions>? options)
|
||||
{
|
||||
this.jsRuntime = jsRuntime;
|
||||
this.themeService = themeService;
|
||||
this.options = options.Value;
|
||||
this.options = options?.Value ?? new CookieThemeServiceOptions();
|
||||
|
||||
themeService.ThemeChanged += OnThemeChanged;
|
||||
if (themeService != null)
|
||||
{
|
||||
themeService.ThemeChanged += OnThemeChanged;
|
||||
}
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
@@ -118,6 +121,13 @@ namespace Radzen
|
||||
|
||||
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{cookie}\"");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
themeService.ThemeChanged -= OnThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
18
Radzen.Blazor/CoordinateSystem.cs
Normal file
18
Radzen.Blazor/CoordinateSystem.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// CoordinateSystem enum
|
||||
/// </summary>
|
||||
public enum CoordinateSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Cartesian CoordinateSystem
|
||||
/// </summary>
|
||||
Cartesian,
|
||||
|
||||
/// <summary>
|
||||
/// Polar CoordinateSystem
|
||||
/// </summary>
|
||||
Polar
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
[Parameter]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the placeholder.
|
||||
/// </summary>
|
||||
/// <value>The placeholder.</value>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; }
|
||||
public string? Placeholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="DataBoundFormComponent{T}"/> is disabled.
|
||||
@@ -80,36 +80,36 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The form
|
||||
/// </summary>
|
||||
IRadzenForm _form;
|
||||
IRadzenForm? form;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the form.
|
||||
/// </summary>
|
||||
/// <value>The form.</value>
|
||||
[CascadingParameter]
|
||||
public IRadzenForm Form
|
||||
public IRadzenForm? Form
|
||||
{
|
||||
get
|
||||
{
|
||||
return _form;
|
||||
return form;
|
||||
}
|
||||
set
|
||||
{
|
||||
_form = value;
|
||||
_form?.AddComponent(this);
|
||||
form = value;
|
||||
form?.AddComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value
|
||||
/// </summary>
|
||||
private T _value = default;
|
||||
private T? _value;
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
[Parameter]
|
||||
public T Value
|
||||
public T? Value
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -167,18 +167,18 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The text property.</value>
|
||||
[Parameter]
|
||||
public string TextProperty { get; set; }
|
||||
public string? TextProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data
|
||||
/// </summary>
|
||||
IEnumerable _data = null;
|
||||
IEnumerable? _data;
|
||||
/// <summary>
|
||||
/// Gets or sets the data.
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
[Parameter]
|
||||
public virtual IEnumerable Data
|
||||
public virtual IEnumerable? Data
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -208,7 +208,7 @@ namespace Radzen
|
||||
/// Gets the query.
|
||||
/// </summary>
|
||||
/// <value>The query.</value>
|
||||
protected virtual IQueryable Query
|
||||
protected virtual IQueryable? Query
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -220,7 +220,7 @@ namespace Radzen
|
||||
/// Gets or sets the search text
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SearchText
|
||||
public string? SearchText
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -245,29 +245,30 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The search text
|
||||
/// </summary>
|
||||
internal string searchText;
|
||||
internal string? searchText;
|
||||
|
||||
/// <summary>
|
||||
/// The view
|
||||
/// </summary>
|
||||
protected IQueryable _view = null;
|
||||
protected IQueryable? _view;
|
||||
/// <summary>
|
||||
/// Gets the view.
|
||||
/// </summary>
|
||||
/// <value>The view.</value>
|
||||
protected virtual IEnumerable View
|
||||
protected virtual IEnumerable? View
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_view == null && Query != null)
|
||||
var query = Query;
|
||||
if (_view == null && query != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(searchText))
|
||||
if (!string.IsNullOrEmpty(searchText) && !string.IsNullOrEmpty(TextProperty))
|
||||
{
|
||||
_view = Query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
_view = query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_view = (typeof(IQueryable).IsAssignableFrom(Data.GetType())) ? Query.Cast<object>().ToList().AsQueryable() : Query;
|
||||
_view = Data is IQueryable ? query.Cast<object>().ToList().AsQueryable() : query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,14 +276,14 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable GetView() => View;
|
||||
internal IEnumerable? GetView() => View;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the edit context.
|
||||
/// </summary>
|
||||
/// <value>The edit context.</value>
|
||||
[CascadingParameter]
|
||||
public EditContext EditContext { get; set; }
|
||||
public EditContext? EditContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the field identifier.
|
||||
@@ -296,7 +297,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The value expression.</value>
|
||||
[Parameter]
|
||||
public Expression<Func<T>> ValueExpression { get; set; }
|
||||
public Expression<Func<T>>? ValueExpression { get; set; }
|
||||
/// <summary>
|
||||
/// Set parameters as an asynchronous operation.
|
||||
/// </summary>
|
||||
@@ -338,7 +339,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="ValidationStateChangedEventArgs"/> instance containing the event data.</param>
|
||||
private void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
|
||||
private void ValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -356,13 +357,15 @@ namespace Radzen
|
||||
}
|
||||
|
||||
Form?.RemoveComponent(this);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
/// <returns>System.Object.</returns>
|
||||
public virtual object GetValue()
|
||||
public virtual object? GetValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
@@ -386,13 +389,13 @@ namespace Radzen
|
||||
|
||||
/// <summary> Provides support for RadzenFormField integration. </summary>
|
||||
[CascadingParameter]
|
||||
public IFormFieldContext FormFieldContext { get; set; }
|
||||
public IFormFieldContext? FormFieldContext { get; set; }
|
||||
|
||||
/// <summary> Gets the current placeholder. Returns empty string if this component is inside a RadzenFormField.</summary>
|
||||
protected string CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
protected string? CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:ContextMenu" /> event.
|
||||
/// Handles the ContextMenu event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <returns>Task.</returns>
|
||||
|
||||
21
Radzen.Blazor/DataGridCellMouseEventArgs.cs
Normal file
21
Radzen.Blazor/DataGridCellMouseEventArgs.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.CellContextMenu" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridCellMouseEventArgs<T> : Microsoft.AspNetCore.Components.Web.MouseEventArgs where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data item which the clicked DataGrid row represents.
|
||||
/// </summary>
|
||||
public T? Data { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RadzenDataGridColumn which this cells represents.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
}
|
||||
|
||||
16
Radzen.Blazor/DataGridCellRenderEventArgs.cs
Normal file
16
Radzen.Blazor/DataGridCellRenderEventArgs.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.CellRender" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridCellRenderEventArgs<T> : RowRenderEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the RadzenDataGridColumn which this cells represents.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
}
|
||||
|
||||
26
Radzen.Blazor/DataGridChildData.cs
Normal file
26
Radzen.Blazor/DataGridChildData.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Internal class for managing hierarchical child data in DataGrid.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
internal class DataGridChildData<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the parent child data.
|
||||
/// </summary>
|
||||
internal DataGridChildData<T>? ParentChildData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
internal int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data.
|
||||
/// </summary>
|
||||
internal IEnumerable<T>? Data { get; set; }
|
||||
}
|
||||
|
||||
41
Radzen.Blazor/DataGridColumnFilterEventArgs.cs
Normal file
41
Radzen.Blazor/DataGridColumnFilterEventArgs.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.Filter" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnFilterEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the filtered RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new filter value of the filtered column.
|
||||
/// </summary>
|
||||
public object? FilterValue { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new second filter value of the filtered column.
|
||||
/// </summary>
|
||||
public object? SecondFilterValue { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new filter operator of the filtered column.
|
||||
/// </summary>
|
||||
public FilterOperator FilterOperator { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new second filter operator of the filtered column.
|
||||
/// </summary>
|
||||
public FilterOperator SecondFilterOperator { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new logical filter operator of the filtered column.
|
||||
/// </summary>
|
||||
public LogicalFilterOperator LogicalFilterOperator { get; internal set; }
|
||||
}
|
||||
|
||||
21
Radzen.Blazor/DataGridColumnGroupEventArgs.cs
Normal file
21
Radzen.Blazor/DataGridColumnGroupEventArgs.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.Group" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnGroupEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the grouped RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group descriptor.
|
||||
/// </summary>
|
||||
public GroupDescriptor? GroupDescriptor { get; internal set; }
|
||||
}
|
||||
|
||||
26
Radzen.Blazor/DataGridColumnReorderedEventArgs.cs
Normal file
26
Radzen.Blazor/DataGridColumnReorderedEventArgs.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.ColumnReordered" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnReorderedEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the reordered RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the old index of the column.
|
||||
/// </summary>
|
||||
public int OldIndex { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new index of the column.
|
||||
/// </summary>
|
||||
public int NewIndex { get; internal set; }
|
||||
}
|
||||
|
||||
27
Radzen.Blazor/DataGridColumnReorderingEventArgs.cs
Normal file
27
Radzen.Blazor/DataGridColumnReorderingEventArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.ColumnReordering" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnReorderingEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the reordered RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reordered to RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? ToColumn { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value which will cancel the event.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> to cancel the event; otherwise, <c>false</c>.</value>
|
||||
public bool Cancel { get; set; }
|
||||
}
|
||||
|
||||
21
Radzen.Blazor/DataGridColumnResizedEventArgs.cs
Normal file
21
Radzen.Blazor/DataGridColumnResizedEventArgs.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.ColumnResized" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnResizedEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the resized RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new width of the resized column.
|
||||
/// </summary>
|
||||
public double Width { get; internal set; }
|
||||
}
|
||||
|
||||
81
Radzen.Blazor/DataGridColumnSettings.cs
Normal file
81
Radzen.Blazor/DataGridColumnSettings.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// DataGrid column settings class used to Save/Load settings.
|
||||
/// </summary>
|
||||
public class DataGridColumnSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Property.
|
||||
/// </summary>
|
||||
public string UniqueID { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Property.
|
||||
/// </summary>
|
||||
public string Property { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Visible.
|
||||
/// </summary>
|
||||
public bool Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Width.
|
||||
/// </summary>
|
||||
public string Width { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// OrderIndex.
|
||||
/// </summary>
|
||||
public int? OrderIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SortOrder.
|
||||
/// </summary>
|
||||
public SortOrder? SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SortIndex.
|
||||
/// </summary>
|
||||
public int? SortIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// FilterValue.
|
||||
/// </summary>
|
||||
public object? FilterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// FilterOperator.
|
||||
/// </summary>
|
||||
public FilterOperator FilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SecondFilterValue.
|
||||
/// </summary>
|
||||
public object? SecondFilterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SecondFilterOperator.
|
||||
/// </summary>
|
||||
public FilterOperator SecondFilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// LogicalFilterOperator.
|
||||
/// </summary>
|
||||
public LogicalFilterOperator LogicalFilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CustomFilterExpression.
|
||||
/// </summary>
|
||||
public string CustomFilterExpression { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mode that determines whether the filter applies to any or all items in a collection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="CollectionFilterMode"/> value indicating whether the filter is satisfied by any or all items.
|
||||
/// </value>
|
||||
public CollectionFilterMode CollectionFilterMode { get; set; }
|
||||
}
|
||||
|
||||
21
Radzen.Blazor/DataGridColumnSortEventArgs.cs
Normal file
21
Radzen.Blazor/DataGridColumnSortEventArgs.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.Sort" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridColumnSortEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the sorted RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new sort order of the sorted column.
|
||||
/// </summary>
|
||||
public SortOrder? SortOrder { get; internal set; }
|
||||
}
|
||||
|
||||
18
Radzen.Blazor/DataGridEditMode.cs
Normal file
18
Radzen.Blazor/DataGridEditMode.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the inline edit mode behavior of <see cref="Radzen.Blazor.RadzenDataGrid{TItem}" />.
|
||||
/// </summary>
|
||||
public enum DataGridEditMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The user can edit only one row at a time. Editing a different row cancels edit mode for the last edited row.
|
||||
/// </summary>
|
||||
Single,
|
||||
|
||||
/// <summary>
|
||||
/// The user can edit multiple rows.
|
||||
/// </summary>
|
||||
Multiple
|
||||
}
|
||||
|
||||
18
Radzen.Blazor/DataGridExpandMode.cs
Normal file
18
Radzen.Blazor/DataGridExpandMode.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the expand behavior of <see cref="Radzen.Blazor.RadzenDataGrid{TItem}" />.
|
||||
/// </summary>
|
||||
public enum DataGridExpandMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The user can expand only one row at a time. Expanding a different row collapses the last expanded row.
|
||||
/// </summary>
|
||||
Single,
|
||||
|
||||
/// <summary>
|
||||
/// The user can expand multiple rows.
|
||||
/// </summary>
|
||||
Multiple
|
||||
}
|
||||
|
||||
33
Radzen.Blazor/DataGridGridLines.cs
Normal file
33
Radzen.Blazor/DataGridGridLines.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the grid lines of <see cref="Radzen.Blazor.RadzenDataGrid{TItem}" />.
|
||||
/// </summary>
|
||||
public enum DataGridGridLines
|
||||
{
|
||||
/// <summary>
|
||||
/// Theme default.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Both horizontal and vertical grid lines.
|
||||
/// </summary>
|
||||
Both,
|
||||
|
||||
/// <summary>
|
||||
/// No grid lines.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal grid lines.
|
||||
/// </summary>
|
||||
Horizontal,
|
||||
|
||||
/// <summary>
|
||||
/// Vertical grid lines.
|
||||
/// </summary>
|
||||
Vertical
|
||||
}
|
||||
|
||||
23
Radzen.Blazor/DataGridLoadChildDataEventArgs.cs
Normal file
23
Radzen.Blazor/DataGridLoadChildDataEventArgs.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="Radzen.Blazor.RadzenDataGrid{TItem}.LoadChildData" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridLoadChildDataEventArgs<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the data.
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
public IEnumerable<T>? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item.
|
||||
/// </summary>
|
||||
/// <value>The item.</value>
|
||||
public T? Item { get; internal set; }
|
||||
}
|
||||
|
||||
53
Radzen.Blazor/DataGridLoadColumnFilterDataEventArgs.cs
Normal file
53
Radzen.Blazor/DataGridLoadColumnFilterDataEventArgs.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="Radzen.Blazor.RadzenDataGrid{TItem}.LoadColumnFilterData" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridLoadColumnFilterDataEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the data.
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
public IEnumerable? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total data count.
|
||||
/// </summary>
|
||||
/// <value>The total data count.</value>
|
||||
public int Count { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets how many items to skip. Related to paging and the current page. Usually used with the <see cref="Enumerable.Skip{TSource}(IEnumerable{TSource}, int)"/> LINQ method.
|
||||
/// </summary>
|
||||
public int? Skip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets how many items to take. Related to paging and the current page size. Usually used with the <see cref="Enumerable.Take{TSource}(IEnumerable{TSource}, int)"/> LINQ method.
|
||||
/// </summary>
|
||||
/// <value>The top.</value>
|
||||
public int? Top { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter expression as a string.
|
||||
/// </summary>
|
||||
/// <value>The filter.</value>
|
||||
public string? Filter { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets filter property used to limit and distinct values, if not set, args.Data are used as values.
|
||||
/// </summary>
|
||||
/// <value>The filter property.</value>
|
||||
public string? Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RadzenDataGridColumn.
|
||||
/// </summary>
|
||||
public Radzen.Blazor.RadzenDataGridColumn<T>? Column { get; internal set; }
|
||||
}
|
||||
|
||||
13
Radzen.Blazor/DataGridLoadSettingsEventArgs.cs
Normal file
13
Radzen.Blazor/DataGridLoadSettingsEventArgs.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="Radzen.Blazor.RadzenDataGrid{TItem}.LoadSettings" /> event that is being raised.
|
||||
/// </summary>
|
||||
public class DataGridLoadSettingsEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the settings.
|
||||
/// </summary>
|
||||
public DataGridSettings? Settings { get; set; }
|
||||
}
|
||||
|
||||
17
Radzen.Blazor/DataGridPickedColumnsChangedEventArgs.cs
Normal file
17
Radzen.Blazor/DataGridPickedColumnsChangedEventArgs.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.PickedColumnsChanged" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridPickedColumnsChangedEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the picked columns.
|
||||
/// </summary>
|
||||
public IEnumerable<RadzenDataGridColumn<T>>? Columns { get; internal set; }
|
||||
}
|
||||
|
||||
22
Radzen.Blazor/DataGridRenderEventArgs.cs
Normal file
22
Radzen.Blazor/DataGridRenderEventArgs.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="RadzenDataGrid{TItem}.Render" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridRenderEventArgs<T> where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the instance of the RadzenDataGrid component which has rendered.
|
||||
/// </summary>
|
||||
public RadzenDataGrid<T>? Grid { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is the first time the RadzenDataGrid has rendered.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this is the first time; otherwise, <c>false</c>.</value>
|
||||
public bool FirstRender { get; internal set; }
|
||||
}
|
||||
|
||||
14
Radzen.Blazor/DataGridRowMouseEventArgs.cs
Normal file
14
Radzen.Blazor/DataGridRowMouseEventArgs.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="Radzen.Blazor.RadzenDataGrid{TItem}.RowClick" /> or <see cref="Radzen.Blazor.RadzenDataGrid{TItem}.RowDoubleClick" /> event that is being raised.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data item type.</typeparam>
|
||||
public class DataGridRowMouseEventArgs<T> : Microsoft.AspNetCore.Components.Web.MouseEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data item which the clicked DataGrid row represents.
|
||||
/// </summary>
|
||||
public T? Data { get; internal set; }
|
||||
}
|
||||
|
||||
18
Radzen.Blazor/DataGridSelectionMode.cs
Normal file
18
Radzen.Blazor/DataGridSelectionMode.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the selection mode behavior of <see cref="Radzen.Blazor.RadzenDataGrid{TItem}" />.
|
||||
/// </summary>
|
||||
public enum DataGridSelectionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The user can select only one row at a time. Selecting a different row deselects the last selected row.
|
||||
/// </summary>
|
||||
Single,
|
||||
|
||||
/// <summary>
|
||||
/// The user can select multiple rows.
|
||||
/// </summary>
|
||||
Multiple
|
||||
}
|
||||
|
||||
30
Radzen.Blazor/DataGridSettings.cs
Normal file
30
Radzen.Blazor/DataGridSettings.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// DataGrid settings class used to Save/Load settings.
|
||||
/// </summary>
|
||||
public class DataGridSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Columns.
|
||||
/// </summary>
|
||||
public IEnumerable<DataGridColumnSettings> Columns { get; set; } = System.Array.Empty<DataGridColumnSettings>();
|
||||
|
||||
/// <summary>
|
||||
/// Groups.
|
||||
/// </summary>
|
||||
public IEnumerable<GroupDescriptor> Groups { get; set; } = System.Array.Empty<GroupDescriptor>();
|
||||
|
||||
/// <summary>
|
||||
/// CurrentPage.
|
||||
/// </summary>
|
||||
public int? CurrentPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PageSize.
|
||||
/// </summary>
|
||||
public int? PageSize { get; set; }
|
||||
}
|
||||
|
||||
34
Radzen.Blazor/DateRenderEventArgs.cs
Normal file
34
Radzen.Blazor/DateRenderEventArgs.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about a <see cref="Radzen.Blazor.RadzenDatePicker{TValue}.DateRender" /> event that is being raised.
|
||||
/// </summary>
|
||||
public class DateRenderEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML attributes that will be applied to item HTML element.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// void OnDateRender(DateRenderEventArgs args)
|
||||
/// {
|
||||
/// args.Attributes["style"] = "background-color: red; color: black;";
|
||||
/// }
|
||||
/// </example>
|
||||
/// <value>The attributes.</value>
|
||||
public IDictionary<string, object> Attributes { get; private set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date which the rendered item represents.
|
||||
/// </summary>
|
||||
public DateTime Date { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the rendered item is disabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if disabled; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -50,14 +51,14 @@ namespace Radzen.Blazor
|
||||
{
|
||||
if (min != null)
|
||||
{
|
||||
var minDate = Convert.ToDateTime(min);
|
||||
var minDate = Convert.ToDateTime(min, CultureInfo.InvariantCulture);
|
||||
Input.Start = minDate.Ticks;
|
||||
Round = false;
|
||||
}
|
||||
|
||||
if (max != null)
|
||||
{
|
||||
var maxDate = Convert.ToDateTime(max);
|
||||
var maxDate = Convert.ToDateTime(max, CultureInfo.InvariantCulture);
|
||||
Input.End = maxDate.Ticks;
|
||||
Round = false;
|
||||
}
|
||||
|
||||
112
Radzen.Blazor/Debouncer.cs
Normal file
112
Radzen.Blazor/Debouncer.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for debouncing and throttling function calls.
|
||||
/// </summary>
|
||||
internal class Debouncer : IDisposable
|
||||
{
|
||||
private System.Timers.Timer? timer;
|
||||
private System.Timers.ElapsedEventHandler? timerElapsedHandler;
|
||||
private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Debounces the specified action.
|
||||
/// </summary>
|
||||
/// <param name="interval">The debounce interval in milliseconds.</param>
|
||||
/// <param name="action">The action to debounce.</param>
|
||||
public void Debounce(int interval, Func<Task> action)
|
||||
{
|
||||
ClearTimer();
|
||||
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
|
||||
timerElapsedHandler = (s, e) =>
|
||||
{
|
||||
if (timer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer?.Stop();
|
||||
timer = null;
|
||||
|
||||
try
|
||||
{
|
||||
Task.Run(action);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
timer.Elapsed += timerElapsedHandler;
|
||||
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throttles the specified action.
|
||||
/// </summary>
|
||||
/// <param name="interval">The throttle interval in milliseconds.</param>
|
||||
/// <param name="action">The action to throttle.</param>
|
||||
public void Throttle(int interval, Func<Task> action)
|
||||
{
|
||||
ClearTimer();
|
||||
var curTime = DateTime.UtcNow;
|
||||
|
||||
if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
|
||||
{
|
||||
interval -= (int)curTime.Subtract(timerStarted).TotalMilliseconds;
|
||||
}
|
||||
|
||||
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
|
||||
timerElapsedHandler = (s, e) =>
|
||||
{
|
||||
if (timer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer?.Stop();
|
||||
timer = null;
|
||||
|
||||
try
|
||||
{
|
||||
Task.Run(action);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
timer.Elapsed += timerElapsedHandler;
|
||||
|
||||
timer.Start();
|
||||
timerStarted = curTime;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ClearTimer();
|
||||
}
|
||||
|
||||
private void ClearTimer()
|
||||
{
|
||||
if (timer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerElapsedHandler != null)
|
||||
{
|
||||
timer.Elapsed -= timerElapsedHandler;
|
||||
timerElapsedHandler = null;
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
timer.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
18
Radzen.Blazor/Density.cs
Normal file
18
Radzen.Blazor/Density.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies component density.
|
||||
/// </summary>
|
||||
public enum Density
|
||||
{
|
||||
/// <summary>
|
||||
/// The default density.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// А high density compact mode.
|
||||
/// </summary>
|
||||
Compact
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
85
Radzen.Blazor/Directory.Build.props
Normal file
85
Radzen.Blazor/Directory.Build.props
Normal file
@@ -0,0 +1,85 @@
|
||||
<Project>
|
||||
<!--
|
||||
Common build properties for all projects in the Radzen.Blazor solution.
|
||||
|
||||
To use this file:
|
||||
1. Rename to Directory.Build.props (remove .sample extension)
|
||||
2. Adjust settings based on your needs
|
||||
3. Review the analyzer settings in .editorconfig
|
||||
|
||||
This file will be automatically imported by all projects in subdirectories.
|
||||
-->
|
||||
|
||||
<PropertyGroup Label="Language Configuration">
|
||||
<!-- Use latest C# language features -->
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- Do NOT enable implicit usings - explicit imports preferred for library code -->
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Code Analysis Configuration">
|
||||
<!-- Enable .NET code analyzers -->
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
|
||||
<!-- Run analyzers during build and in IDE -->
|
||||
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
|
||||
|
||||
<!-- Don't enforce code style in build (yet) - just show warnings -->
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
|
||||
<!-- Don't treat warnings as errors (yet) - too many to fix immediately -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
|
||||
<!-- Report all analyzer diagnostics -->
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Build Quality">
|
||||
<!-- Enable deterministic builds for reproducibility -->
|
||||
<Deterministic>true</Deterministic>
|
||||
|
||||
<!-- Enable deterministic builds in CI/CD -->
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
|
||||
<!-- Embed source files for better debugging -->
|
||||
<EmbedAllSources>true</EmbedAllSources>
|
||||
<!--
|
||||
IMPORTANT:
|
||||
- NuGet symbol packages (.snupkg) require portable PDB files.
|
||||
- If DebugType=embedded, there are no standalone PDBs, so the .snupkg ends up effectively empty.
|
||||
Use portable PDBs when symbols are enabled; otherwise use embedded for local debugging convenience.
|
||||
-->
|
||||
<!--
|
||||
NOTE: Directory.Build.props is imported before project files, so properties like IncludeSymbols
|
||||
set in a .csproj may not be available yet for Conditions here.
|
||||
IsPacking *is* set by `dotnet pack`, so use that to switch DebugType for symbol packages.
|
||||
-->
|
||||
<DebugType Condition="'$(IsPacking)' == 'true'">portable</DebugType>
|
||||
<DebugType Condition="'$(IsPacking)' != 'true'">embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Demos and Tests Project Configuration" Condition="$(MSBuildProjectName.Contains('Demos')) OR $(MSBuildProjectName.Contains('Tests'))">
|
||||
<!-- Demo projects and Tests should not be packable -->
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<!-- DISABLE ALL ANALYZERS FOR DEMO PROJECTS AND TESTS -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Performance">
|
||||
<!-- Optimize startup time -->
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.JSInterop;
|
||||
using Radzen.Blazor;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -22,12 +21,12 @@ namespace Radzen
|
||||
[Parameter]
|
||||
public int VirtualizationOverscanCount { get; set; }
|
||||
|
||||
internal Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<object> virtualize;
|
||||
internal Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<object>? virtualize;
|
||||
|
||||
/// <summary>
|
||||
/// The Virtualize instance.
|
||||
/// </summary>
|
||||
public Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<object> Virtualize
|
||||
public Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize<object>? Virtualize
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -35,12 +34,12 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
List<object> virtualItems;
|
||||
List<object>? virtualItems;
|
||||
|
||||
private async ValueTask<Microsoft.AspNetCore.Components.Web.Virtualization.ItemsProviderResult<object>> LoadItems(Microsoft.AspNetCore.Components.Web.Virtualization.ItemsProviderRequest request)
|
||||
{
|
||||
var data = Data != null ? Data.Cast<object>() : Enumerable.Empty<object>();
|
||||
var view = (LoadData.HasDelegate ? data : View).Cast<object>().AsQueryable();
|
||||
var view = (LoadData.HasDelegate ? data : View)?.Cast<object>().AsQueryable() ?? Enumerable.Empty<object>().AsQueryable();
|
||||
var totalItemsCount = LoadData.HasDelegate ? Count : view.Count();
|
||||
var top = request.Count;
|
||||
|
||||
@@ -54,7 +53,7 @@ namespace Radzen
|
||||
await LoadData.InvokeAsync(new Radzen.LoadDataArgs() { Skip = request.StartIndex, Top = top, Filter = searchText });
|
||||
}
|
||||
|
||||
virtualItems = (LoadData.HasDelegate ? Data : view.Skip(request.StartIndex).Take(top)).Cast<object>().ToList();
|
||||
virtualItems = (LoadData.HasDelegate ? (Data ?? Enumerable.Empty<object>()) : view.Skip(request.StartIndex).Take(top)).Cast<object>().ToList();
|
||||
|
||||
return new Microsoft.AspNetCore.Components.Web.Virtualization.ItemsProviderResult<object>(virtualItems, LoadData.HasDelegate ? Count : totalItemsCount);
|
||||
}
|
||||
@@ -105,13 +104,13 @@ namespace Radzen
|
||||
builder.AddAttribute(1, "ItemsProvider", new Microsoft.AspNetCore.Components.Web.Virtualization.ItemsProviderDelegate<object>(LoadItems));
|
||||
builder.AddAttribute(2, "ChildContent", (RenderFragment<object>)((context) =>
|
||||
{
|
||||
return (RenderFragment)((b) =>
|
||||
return b =>
|
||||
{
|
||||
RenderItem(b, context);
|
||||
});
|
||||
};
|
||||
}));
|
||||
|
||||
if (VirtualizationOverscanCount != default(int))
|
||||
if (VirtualizationOverscanCount != default)
|
||||
{
|
||||
builder.AddAttribute(3, "OverscanCount", VirtualizationOverscanCount);
|
||||
}
|
||||
@@ -122,9 +121,13 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in LoadData.HasDelegate ? Data : View)
|
||||
var items = LoadData.HasDelegate ? Data : View;
|
||||
if (items != null)
|
||||
{
|
||||
RenderItem(builder, item);
|
||||
foreach (var item in items)
|
||||
{
|
||||
RenderItem(builder, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -160,21 +163,18 @@ namespace Radzen
|
||||
//
|
||||
}
|
||||
|
||||
System.Collections.Generic.HashSet<object> keys = new System.Collections.Generic.HashSet<object>();
|
||||
HashSet<object> keys = new HashSet<object>();
|
||||
|
||||
internal object GetKey(object item)
|
||||
internal object? GetKey(object item)
|
||||
{
|
||||
var value = GetItemOrValueFromProperty(item, ValueProperty);
|
||||
var value = GetItemOrValueFromProperty(item, ValueProperty ?? string.Empty);
|
||||
|
||||
if (!keys.Contains(value))
|
||||
if (value != null)
|
||||
{
|
||||
keys.Add(value);
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,7 +182,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The header template.</value>
|
||||
[Parameter]
|
||||
public RenderFragment HeaderTemplate { get; set; }
|
||||
public RenderFragment? HeaderTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether filtering is allowed. Set to <c>false</c> by default.
|
||||
@@ -231,21 +231,21 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The template.</value>
|
||||
[Parameter]
|
||||
public RenderFragment<dynamic> Template { get; set; }
|
||||
public RenderFragment<dynamic>? Template { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value property.
|
||||
/// </summary>
|
||||
/// <value>The value property.</value>
|
||||
[Parameter]
|
||||
public string ValueProperty { get; set; }
|
||||
public string? ValueProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disabled property.
|
||||
/// </summary>
|
||||
/// <value>The disabled property.</value>
|
||||
[Parameter]
|
||||
public string DisabledProperty { get; set; }
|
||||
public string? DisabledProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the remove chip button title.
|
||||
@@ -254,6 +254,13 @@ namespace Radzen
|
||||
[Parameter]
|
||||
public string RemoveChipTitle { get; set; } = "Remove";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the clear button aria label text.
|
||||
/// </summary>
|
||||
/// <value>The clear button aria label text.</value>
|
||||
[Parameter]
|
||||
public string ClearAriaLabel { get; set; } = "Clear";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search aria label text.
|
||||
/// </summary>
|
||||
@@ -282,7 +289,7 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The selected item
|
||||
/// </summary>
|
||||
protected object selectedItem = null;
|
||||
protected object? selectedItem;
|
||||
Type GetItemType(IEnumerable items)
|
||||
{
|
||||
var firstType = items.Cast<object>().FirstOrDefault()?.GetType() ?? typeof(object);
|
||||
@@ -301,7 +308,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
protected virtual async System.Threading.Tasks.Task SelectAll()
|
||||
{
|
||||
if (Disabled)
|
||||
if (Disabled || View == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -316,11 +323,14 @@ namespace Radzen
|
||||
selectedItems.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
if (!string.IsNullOrEmpty(ValueProperty) && Data != null)
|
||||
{
|
||||
var elementType = PropertyAccess.GetElementType(Data.GetType());
|
||||
System.Reflection.PropertyInfo pi = PropertyAccess.GetProperty(elementType, ValueProperty);
|
||||
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
|
||||
System.Reflection.PropertyInfo? pi = PropertyAccess.GetProperty(elementType, ValueProperty);
|
||||
if (pi != null)
|
||||
{
|
||||
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -329,25 +339,34 @@ namespace Radzen
|
||||
internalValue = selectedItems.AsQueryable().Cast(type);
|
||||
}
|
||||
|
||||
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
|
||||
if (internalValue != null)
|
||||
{
|
||||
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
|
||||
}
|
||||
if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); }
|
||||
await Change.InvokeAsync(internalValue);
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", GetId());
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", GetId());
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsAllSelected()
|
||||
{
|
||||
List<object> notDisabledItemsInList = View.Cast<object>().ToList()
|
||||
List<object> notDisabledItemsInList = View != null ? View.Cast<object>().ToList()
|
||||
.Where(i => disabledPropertyGetter == null || disabledPropertyGetter(i) as bool? != true)
|
||||
.ToList();
|
||||
.ToList() : new List<object>();
|
||||
|
||||
if (LoadData.HasDelegate && !string.IsNullOrEmpty(ValueProperty))
|
||||
{
|
||||
return View != null && notDisabledItemsInList.Count > 0 && notDisabledItemsInList
|
||||
.All(i => IsItemSelectedByValue(GetItemOrValueFromProperty(i, ValueProperty)));
|
||||
.All(i => {
|
||||
var value = GetItemOrValueFromProperty(i, ValueProperty);
|
||||
return value != null ? IsItemSelectedByValue(value) : false;
|
||||
});
|
||||
}
|
||||
|
||||
return View != null && notDisabledItemsInList.Count > 0 && selectedItems.Count == notDisabledItemsInList.Count;
|
||||
@@ -365,14 +384,17 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// Clears all.
|
||||
/// </summary>
|
||||
protected async System.Threading.Tasks.Task ClearAll()
|
||||
protected async Task ClearAll()
|
||||
{
|
||||
if (Disabled)
|
||||
return;
|
||||
|
||||
searchText = null;
|
||||
await SearchTextChanged.InvokeAsync(searchText);
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, "");
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, "");
|
||||
}
|
||||
|
||||
internalValue = collectionAssignment.GetCleared();
|
||||
selectedItem = null;
|
||||
@@ -381,7 +403,7 @@ namespace Radzen
|
||||
|
||||
selectedIndex = -1;
|
||||
|
||||
await ValueChanged.InvokeAsync((T)internalValue);
|
||||
await ValueChanged.InvokeAsync(internalValue != null ? (T)internalValue : default(T)!);
|
||||
if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); }
|
||||
await Change.InvokeAsync(internalValue);
|
||||
|
||||
@@ -393,14 +415,14 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The data
|
||||
/// </summary>
|
||||
IEnumerable _data;
|
||||
IEnumerable? _data;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data.
|
||||
/// </summary>
|
||||
/// <value>The data.</value>
|
||||
[Parameter]
|
||||
public override IEnumerable Data
|
||||
public override IEnumerable? Data
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -466,22 +488,26 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
Func<object, object> GetGetter(string propertyName, Type type)
|
||||
Func<object, object?> GetGetter(string propertyName, Type type)
|
||||
{
|
||||
if (propertyName?.Contains("[") == true)
|
||||
if (propertyName?.Contains('[', StringComparison.Ordinal) == true)
|
||||
{
|
||||
var getter = typeof(PropertyAccess).GetMethod("Getter", [typeof(string), typeof(Type)]);
|
||||
if (getter == null)
|
||||
{
|
||||
return PropertyAccess.Getter<object, object?>(propertyName, type);
|
||||
}
|
||||
var getterMethod = getter.MakeGenericMethod([type, typeof(object)]);
|
||||
|
||||
return (i) => getterMethod.Invoke(i, [propertyName, type]);
|
||||
return (i) => getterMethod.Invoke(i, [propertyName, type])!;
|
||||
}
|
||||
|
||||
return PropertyAccess.Getter<object, object>(propertyName, type);
|
||||
return PropertyAccess.Getter<object, object?>(propertyName ?? string.Empty, type);
|
||||
}
|
||||
|
||||
internal Func<object, object> valuePropertyGetter;
|
||||
internal Func<object, object> textPropertyGetter;
|
||||
internal Func<object, object> disabledPropertyGetter;
|
||||
internal Func<object, object?>? valuePropertyGetter;
|
||||
internal Func<object, object?>? textPropertyGetter;
|
||||
internal Func<object, object?>? disabledPropertyGetter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item or value from property.
|
||||
@@ -489,7 +515,7 @@ namespace Radzen
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="property">The property.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object GetItemOrValueFromProperty(object item, string property)
|
||||
public virtual object? GetItemOrValueFromProperty(object? item, string property)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
@@ -539,6 +565,18 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the listbox identifier.
|
||||
/// </summary>
|
||||
/// <value>The listbox identifier.</value>
|
||||
protected string ListId
|
||||
{
|
||||
get
|
||||
{
|
||||
return $"{GetId()}-list";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search identifier.
|
||||
/// </summary>
|
||||
@@ -610,16 +648,21 @@ namespace Radzen
|
||||
if (Disabled)
|
||||
return;
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.togglePopup", Element, PopupID, true);
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", isFilter ? UniqueID : SearchID);
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.togglePopup", Element, PopupID, true);
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", isFilter ? UniqueID : SearchID);
|
||||
}
|
||||
|
||||
if (list != null)
|
||||
isPopupOpen = true;
|
||||
|
||||
if (list != null && JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.selectListItem", search, list, selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool preventKeydown = false;
|
||||
internal bool preventKeydown;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the key press.
|
||||
@@ -627,9 +670,13 @@ namespace Radzen
|
||||
/// <param name="args">The <see cref="Microsoft.AspNetCore.Components.Web.KeyboardEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="isFilter">if set to <c>true</c> [is filter].</param>
|
||||
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
|
||||
protected virtual async System.Threading.Tasks.Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
|
||||
protected virtual async Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
|
||||
{
|
||||
if (Disabled || Data == null)
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
|
||||
var key = args.Code != null ? args.Code : args.Key;
|
||||
|
||||
if (Disabled || Data == null || args == null || key == null)
|
||||
return;
|
||||
|
||||
List<object> items = Enumerable.Empty<object>().ToList();
|
||||
@@ -649,28 +696,29 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
items = View.Cast<object>().ToList();
|
||||
items = View != null ? View.Cast<object>().ToList() : Enumerable.Empty<object>().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var key = args.Code != null ? args.Code : args.Key;
|
||||
|
||||
if (!args.AltKey && (key == "ArrowDown" || key == "ArrowLeft" || key == "ArrowUp" || key == "ArrowRight"))
|
||||
{
|
||||
preventKeydown = true;
|
||||
|
||||
try
|
||||
{
|
||||
selectedIndex = await JSRuntime.InvokeAsync<int>("Radzen.focusListItem", search, list, key == "ArrowDown" || key == "ArrowRight", selectedIndex);
|
||||
|
||||
var popupOpened = await JSRuntime.InvokeAsync<bool>("Radzen.popupOpened", PopupID);
|
||||
|
||||
if (!Multiple && !popupOpened && shouldSelectOnChange != false)
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
var itemToSelect = items.ElementAtOrDefault(selectedIndex);
|
||||
if (itemToSelect != null)
|
||||
selectedIndex = await JSRuntime.InvokeAsync<int>("Radzen.focusListItem", search, list, key == "ArrowDown" || key == "ArrowRight", selectedIndex);
|
||||
|
||||
var popupOpened = await JSRuntime.InvokeAsync<bool>("Radzen.popupOpened", PopupID);
|
||||
|
||||
if (!Multiple && !popupOpened && shouldSelectOnChange != false)
|
||||
{
|
||||
await OnSelectItem(itemToSelect, true);
|
||||
var itemToSelect = items.ElementAtOrDefault(selectedIndex);
|
||||
if (itemToSelect != null)
|
||||
{
|
||||
await OnSelectItem(itemToSelect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,32 +731,40 @@ namespace Radzen
|
||||
{
|
||||
preventKeydown = true;
|
||||
|
||||
if (selectedIndex >= 0 && selectedIndex <= items.Count() - 1)
|
||||
if (selectedIndex == -1 && items.Count == 1)
|
||||
{
|
||||
var itemToSelect = items.ElementAtOrDefault(selectedIndex);
|
||||
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, $"{searchText}".Trim());
|
||||
|
||||
if (itemToSelect != null)
|
||||
{
|
||||
await OnSelectItem(itemToSelect, true);
|
||||
}
|
||||
selectedIndex = 0;
|
||||
}
|
||||
|
||||
var popupOpened = await JSRuntime.InvokeAsync<bool>("Radzen.popupOpened", PopupID);
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
if (selectedIndex >= 0 && selectedIndex <= items.Count - 1)
|
||||
{
|
||||
var itemToSelect = items.ElementAtOrDefault(selectedIndex);
|
||||
|
||||
if (!popupOpened)
|
||||
{
|
||||
if(key != "Space")
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, $"{searchText}".Trim());
|
||||
|
||||
if (itemToSelect != null)
|
||||
{
|
||||
await OnSelectItem(itemToSelect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Multiple && !isFilter)
|
||||
|
||||
var popupOpened = await JSRuntime.InvokeAsync<bool>("Radzen.popupOpened", PopupID);
|
||||
|
||||
if (!popupOpened)
|
||||
{
|
||||
await ClosePopup(key);
|
||||
if (key != "Space")
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Multiple && (!isFilter || key != "Space"))
|
||||
{
|
||||
await ClosePopup(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,11 +809,20 @@ namespace Radzen
|
||||
else if (args.Key.Length == 1 && !args.CtrlKey && !args.AltKey && !args.ShiftKey)
|
||||
{
|
||||
// searching for element
|
||||
if (Query == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var query = Query;
|
||||
var elementType = query.ElementType;
|
||||
if (elementType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var filteredItems = (!string.IsNullOrEmpty(TextProperty) ?
|
||||
Query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive) :
|
||||
Query)
|
||||
.Cast(Query.ElementType).Cast<dynamic>().ToList();
|
||||
|
||||
query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive) :
|
||||
query)
|
||||
.Cast(elementType).Cast<dynamic>().ToList();
|
||||
|
||||
if (previousKey != args.Key)
|
||||
{
|
||||
@@ -765,7 +830,7 @@ namespace Radzen
|
||||
itemIndex = -1;
|
||||
}
|
||||
|
||||
itemIndex = itemIndex + 1 >= filteredItems.Count() ? 0 : itemIndex + 1;
|
||||
itemIndex = itemIndex + 1 >= filteredItems.Count ? 0 : itemIndex + 1;
|
||||
var itemToSelect = filteredItems.ElementAtOrDefault(itemIndex);
|
||||
|
||||
if (itemToSelect is not null)
|
||||
@@ -782,7 +847,10 @@ namespace Radzen
|
||||
{
|
||||
selectedIndex = result.Index;
|
||||
}
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.selectListItem", list, list, result.Index);
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.selectListItem", list, list, result.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,14 +860,24 @@ namespace Radzen
|
||||
|
||||
internal virtual async Task ClosePopup(string key)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
|
||||
}
|
||||
|
||||
isPopupOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the popup is open.
|
||||
/// </summary>
|
||||
protected bool isPopupOpen;
|
||||
|
||||
int itemIndex;
|
||||
string previousKey;
|
||||
string? previousKey;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:FilterKeyPress" /> event.
|
||||
/// Handles the FilterKeyPress event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="Microsoft.AspNetCore.Components.Web.KeyboardEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual async System.Threading.Tasks.Task OnFilterKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args)
|
||||
@@ -849,13 +927,15 @@ namespace Radzen
|
||||
|
||||
if (Multiple)
|
||||
selectedIndex = -1;
|
||||
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.repositionPopup", Element, PopupID);
|
||||
if (JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.repositionPopup", Element, PopupID);
|
||||
}
|
||||
await InvokeAsync(() => SearchTextChanged.InvokeAsync(SearchText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:KeyPress" /> event.
|
||||
/// Handles the KeyPress event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="Microsoft.AspNetCore.Components.Web.KeyboardEventArgs"/> instance containing the event data.</param>
|
||||
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
|
||||
@@ -869,13 +949,13 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="isFromKey">if set to <c>true</c> [is from key].</param>
|
||||
protected virtual async System.Threading.Tasks.Task OnSelectItem(object item, bool isFromKey = false)
|
||||
protected virtual async System.Threading.Tasks.Task OnSelectItem(object? item, bool isFromKey = false)
|
||||
{
|
||||
await SelectItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:Filter" /> event.
|
||||
/// Handles the Filter event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual async System.Threading.Tasks.Task OnFilter(ChangeEventArgs args)
|
||||
@@ -886,6 +966,25 @@ namespace Radzen
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles filter input changes (e.g. paste).
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual async Task OnFilterInput(ChangeEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
|
||||
searchText = $"{args.Value}";
|
||||
await SearchTextChanged.InvokeAsync(searchText);
|
||||
|
||||
if (ResetSelectedIndexOnFilter)
|
||||
{
|
||||
selectedIndex = -1;
|
||||
}
|
||||
|
||||
Debounce(DebounceFilter, FilterDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the load data arguments.
|
||||
/// </summary>
|
||||
@@ -943,7 +1042,7 @@ namespace Radzen
|
||||
if (valueChanged)
|
||||
{
|
||||
internalValue = parameters.GetValueOrDefault<object>(nameof(Value));
|
||||
if (PreserveCollectionOnSelection)
|
||||
if (PreserveCollectionOnSelection && internalValue != null)
|
||||
{
|
||||
collectionAssignment = new ReferenceGenericCollectionAssignment((T)internalValue);
|
||||
}
|
||||
@@ -966,7 +1065,7 @@ namespace Radzen
|
||||
}
|
||||
|
||||
var shouldClose = visibleChanged && !Visible;
|
||||
if (shouldClose && !firstRender)
|
||||
if (shouldClose && !firstRender && JSRuntime != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("Radzen.destroyPopup", PopupID);
|
||||
}
|
||||
@@ -987,19 +1086,26 @@ namespace Radzen
|
||||
selectedItems.Clear();
|
||||
}
|
||||
}
|
||||
else if (internalValue == null && Multiple && selectedItems.Count > 0)
|
||||
{
|
||||
selectedItems.Clear();
|
||||
}
|
||||
|
||||
SelectItemFromValue(internalValue);
|
||||
SelectItemFromValue(internalValue);
|
||||
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:Change" /> event.
|
||||
/// Handles the Change event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>
|
||||
protected void OnChange(ChangeEventArgs args)
|
||||
{
|
||||
internalValue = args.Value;
|
||||
if (args != null)
|
||||
{
|
||||
internalValue = args.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1007,17 +1113,18 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if the specified item is selected; otherwise, <c>false</c>.</returns>
|
||||
internal bool IsSelected(object item)
|
||||
internal bool IsSelected(object? item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
{
|
||||
return IsItemSelectedByValue(GetItemOrValueFromProperty(item, ValueProperty));
|
||||
var value = GetItemOrValueFromProperty(item, ValueProperty);
|
||||
return value != null ? IsItemSelectedByValue(value) : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Multiple)
|
||||
{
|
||||
return selectedItems.Contains(item);
|
||||
return selectedItems.Contains(item!);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1031,7 +1138,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The selected item.</value>
|
||||
[Parameter]
|
||||
public object SelectedItem
|
||||
public object? SelectedItem
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -1069,13 +1176,13 @@ namespace Radzen
|
||||
/// Gets the view.
|
||||
/// </summary>
|
||||
/// <value>The view.</value>
|
||||
protected override IEnumerable View
|
||||
protected override IEnumerable? View
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_view == null && Query != null)
|
||||
{
|
||||
_view = Query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
_view = Query.Where(TextProperty ?? string.Empty, searchText, FilterOperator, FilterCaseSensitivity);
|
||||
}
|
||||
|
||||
return _view;
|
||||
@@ -1087,7 +1194,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
void SetSelectedIndexFromSelectedItem()
|
||||
{
|
||||
if (selectedItem != null)
|
||||
if (selectedItem != null && View != null)
|
||||
{
|
||||
if (typeof(EnumerableQuery).IsAssignableFrom(View.GetType()))
|
||||
{
|
||||
@@ -1109,17 +1216,17 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="raiseChange">if set to <c>true</c> [raise change].</param>
|
||||
internal async System.Threading.Tasks.Task SelectItemInternal(object item, bool raiseChange = true)
|
||||
internal async Task SelectItemInternal(object item, bool raiseChange = true)
|
||||
{
|
||||
await SelectItem(item, raiseChange);
|
||||
}
|
||||
|
||||
internal object internalValue;
|
||||
internal object? internalValue;
|
||||
|
||||
/// <summary>
|
||||
/// Will add/remove selected items from a bound ICollection<T>, instead of replacing it.
|
||||
/// </summary>
|
||||
protected bool PreserveCollectionOnSelection = false;
|
||||
protected bool PreserveCollectionOnSelection;
|
||||
private DefaultCollectionAssignment collectionAssignment = new();
|
||||
|
||||
/// <summary>
|
||||
@@ -1127,7 +1234,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="raiseChange">if set to <c>true</c> [raise change].</param>
|
||||
public async System.Threading.Tasks.Task SelectItem(object item, bool raiseChange = true)
|
||||
public async Task SelectItem(object? item, bool raiseChange = true)
|
||||
{
|
||||
if (disabledPropertyGetter != null && item != null && disabledPropertyGetter(item) as bool? == true)
|
||||
{
|
||||
@@ -1140,7 +1247,7 @@ namespace Radzen
|
||||
return;
|
||||
|
||||
selectedItem = item;
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
if (!string.IsNullOrEmpty(ValueProperty) && item != null)
|
||||
{
|
||||
internalValue = PropertyAccess.GetItemOrValueFromProperty(item, ValueProperty);
|
||||
}
|
||||
@@ -1155,16 +1262,23 @@ namespace Radzen
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSelectedItems(item);
|
||||
UpdateSelectedItems(item!);
|
||||
|
||||
if (!string.IsNullOrEmpty(ValueProperty))
|
||||
if (!string.IsNullOrEmpty(ValueProperty) && Data != null)
|
||||
{
|
||||
var elementType = PropertyAccess.GetElementType(Data.GetType());
|
||||
System.Reflection.PropertyInfo pi = PropertyAccess.GetProperty(elementType, ValueProperty);
|
||||
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
|
||||
System.Reflection.PropertyInfo? pi = PropertyAccess.GetProperty(elementType, ValueProperty);
|
||||
if(pi != null)
|
||||
{
|
||||
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var query = Data.AsQueryable();
|
||||
var elementType = query.ElementType;
|
||||
|
||||
@@ -1193,11 +1307,14 @@ namespace Radzen
|
||||
{
|
||||
if (Multiple)
|
||||
{
|
||||
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
|
||||
if (internalValue != null)
|
||||
{
|
||||
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ValueChanged.InvokeAsync((T)internalValue);
|
||||
await ValueChanged.InvokeAsync(internalValue != null ? (T)internalValue : default(T)!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,11 +1322,27 @@ namespace Radzen
|
||||
|
||||
await Change.InvokeAsync(internalValue);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles keyboard activation for the select-all action.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="Microsoft.AspNetCore.Components.Web.KeyboardEventArgs"/> instance containing the event data.</param>
|
||||
protected async Task OnSelectAllKeyDown(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
|
||||
var key = args.Code != null ? args.Code : args.Key;
|
||||
if (key == "Enter" || key == "Space")
|
||||
{
|
||||
await SelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue()
|
||||
public override object? GetValue()
|
||||
{
|
||||
return internalValue;
|
||||
}
|
||||
@@ -1220,7 +1353,7 @@ namespace Radzen
|
||||
{
|
||||
var value = GetItemOrValueFromProperty(item, ValueProperty);
|
||||
|
||||
if (!IsItemSelectedByValue(value))
|
||||
if (value != null && !IsItemSelectedByValue(value))
|
||||
{
|
||||
selectedItems.Add(item);
|
||||
}
|
||||
@@ -1242,7 +1375,7 @@ namespace Radzen
|
||||
/// Selects the item from value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
protected virtual void SelectItemFromValue(object value)
|
||||
protected virtual void SelectItemFromValue(object? value)
|
||||
{
|
||||
var view = LoadData.HasDelegate ? Data : View;
|
||||
if (value != null && view != null)
|
||||
@@ -1289,7 +1422,7 @@ namespace Radzen
|
||||
|
||||
if (typeof(EnumerableQuery).IsAssignableFrom(view.GetType()))
|
||||
{
|
||||
item = view.OfType<object>().Where(i => object.Equals(GetItemOrValueFromProperty(i, ValueProperty), v)).FirstOrDefault();
|
||||
item = view.OfType<object>().Where(i => object.Equals(GetItemOrValueFromProperty(i, ValueProperty), v)).FirstOrDefault()!;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1302,7 +1435,7 @@ namespace Radzen
|
||||
}
|
||||
},
|
||||
LogicalFilterOperator.And,
|
||||
FilterCaseSensitivity.Default).FirstOrDefault();
|
||||
FilterCaseSensitivity.Default).FirstOrDefault()!;
|
||||
}
|
||||
|
||||
if (!object.Equals(item, null) && !selectedItems.AsQueryable().Where(i => object.Equals(GetItemOrValueFromProperty(i, ValueProperty), v)).Any())
|
||||
@@ -1327,7 +1460,7 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// For lists of objects, an IEqualityComparer to control how selected items are determined
|
||||
/// </summary>
|
||||
[Parameter] public IEqualityComparer<object> ItemComparer { get; set; }
|
||||
[Parameter] public IEqualityComparer<object>? ItemComparer { get; set; }
|
||||
|
||||
internal bool IsItemSelectedByValue(object v)
|
||||
{
|
||||
@@ -1350,6 +1483,8 @@ namespace Radzen
|
||||
base.Dispose();
|
||||
|
||||
keys.Clear();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private class DefaultCollectionAssignment
|
||||
@@ -1360,42 +1495,55 @@ namespace Radzen
|
||||
{
|
||||
if (object.Equals(selectedItems, null))
|
||||
{
|
||||
await valueChanged.InvokeAsync(default(T));
|
||||
await valueChanged.InvokeAsync(default(T)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
var list = (IList)Activator.CreateInstance(typeof(T));
|
||||
foreach (var i in (IEnumerable)selectedItems)
|
||||
var list = (IList?)Activator.CreateInstance<T>();
|
||||
if (list != null)
|
||||
{
|
||||
list.Add(i);
|
||||
foreach (var i in (IEnumerable)selectedItems)
|
||||
{
|
||||
list.Add(i);
|
||||
}
|
||||
await valueChanged.InvokeAsync((T)(object)list);
|
||||
}
|
||||
else
|
||||
{
|
||||
await valueChanged.InvokeAsync(default(T)!);
|
||||
}
|
||||
await valueChanged.InvokeAsync((T)(object)list);
|
||||
}
|
||||
}
|
||||
else if (typeof(T).IsGenericType && typeof(ICollection<>).MakeGenericType(typeof(T).GetGenericArguments()[0]).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
if (object.Equals(selectedItems, null))
|
||||
{
|
||||
await valueChanged.InvokeAsync(default(T));
|
||||
await valueChanged.InvokeAsync(default(T)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(typeof(T).GetGenericArguments()[0]));
|
||||
var list = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(typeof(T).GetGenericArguments()[0]));
|
||||
if (list != null)
|
||||
{
|
||||
foreach (var i in (IEnumerable)selectedItems)
|
||||
{
|
||||
list.Add(i);
|
||||
}
|
||||
await valueChanged.InvokeAsync((T)(object)list);
|
||||
}
|
||||
else
|
||||
{
|
||||
await valueChanged.InvokeAsync(default(T)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await valueChanged.InvokeAsync(object.Equals(selectedItems, null) ? default(T) : (T)selectedItems);
|
||||
await valueChanged.InvokeAsync(object.Equals(selectedItems, null) ? default(T)! : (T)selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual T GetCleared()
|
||||
public virtual T? GetCleared()
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
@@ -1405,9 +1553,9 @@ namespace Radzen
|
||||
{
|
||||
private readonly T originalCollection;
|
||||
private readonly bool canHandle;
|
||||
private readonly System.Reflection.MethodInfo clearMethod;
|
||||
private readonly System.Reflection.MethodInfo addMethod;
|
||||
private readonly System.Reflection.MethodInfo removeMethod;
|
||||
private readonly System.Reflection.MethodInfo? clearMethod;
|
||||
private readonly System.Reflection.MethodInfo? addMethod;
|
||||
private readonly System.Reflection.MethodInfo? removeMethod;
|
||||
|
||||
public ReferenceGenericCollectionAssignment(T originalCollection)
|
||||
{
|
||||
@@ -1434,34 +1582,34 @@ namespace Radzen
|
||||
|
||||
public override async Task MakeAssignment(IEnumerable selectedItems, EventCallback<T> valueChanged)
|
||||
{
|
||||
if (!canHandle)
|
||||
if (!canHandle || originalCollection == null)
|
||||
{
|
||||
// Fallback to default behavior when we can't handle the type
|
||||
// Fallback to default behavior when we can't handle the type or originalCollection is null
|
||||
await base.MakeAssignment(selectedItems, valueChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentItems = selectedItems.Cast<object>().ToHashSet();
|
||||
var existingItems = ((IEnumerable)originalCollection).Cast<object>().ToHashSet();
|
||||
var existingItems = (originalCollection as IEnumerable)?.Cast<object>().ToHashSet() ?? new HashSet<object>();
|
||||
foreach (var i in currentItems)
|
||||
{
|
||||
if (!existingItems.Contains(i))
|
||||
addMethod.Invoke(originalCollection, [i]);
|
||||
addMethod!.Invoke(originalCollection, [i]);
|
||||
}
|
||||
foreach (var i in existingItems)
|
||||
{
|
||||
if (!currentItems.Contains(i))
|
||||
removeMethod.Invoke(originalCollection, [i]);
|
||||
removeMethod!.Invoke(originalCollection, [i]);
|
||||
}
|
||||
|
||||
await valueChanged.InvokeAsync(originalCollection);
|
||||
}
|
||||
|
||||
public override T GetCleared()
|
||||
public override T? GetCleared()
|
||||
{
|
||||
if (canHandle)
|
||||
if (canHandle && originalCollection != null)
|
||||
{
|
||||
clearMethod.Invoke(originalCollection, null);
|
||||
clearMethod!.Invoke(originalCollection, null);
|
||||
return originalCollection;
|
||||
}
|
||||
return base.GetCleared();
|
||||
|
||||
32
Radzen.Blazor/DropDownBaseItemRenderEventArgs.cs
Normal file
32
Radzen.Blazor/DropDownBaseItemRenderEventArgs.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about RadzenDropDown ItemRender event.
|
||||
/// </summary>
|
||||
public class DropDownBaseItemRenderEventArgs<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data item.
|
||||
/// </summary>
|
||||
public object? Item { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this item is visible.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
||||
public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this item is disabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if disabled; otherwise, <c>false</c>.</value>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the row HTML attributes.
|
||||
/// </summary>
|
||||
public IDictionary<string, object> Attributes { get; private set; } = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
15
Radzen.Blazor/DropDownItemRenderEventArgs.cs
Normal file
15
Radzen.Blazor/DropDownItemRenderEventArgs.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Radzen.Blazor;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies information about RadzenDropDown ItemRender event.
|
||||
/// </summary>
|
||||
public class DropDownItemRenderEventArgs<TValue> : DropDownBaseItemRenderEventArgs<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the DropDown.
|
||||
/// </summary>
|
||||
public RadzenDropDown<TValue>? DropDown { get; internal set; }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@ namespace System.Linq.Dynamic.Core
|
||||
/// </summary>
|
||||
public static class DynamicExtensions
|
||||
{
|
||||
static readonly Func<string, Type> typeLocator = type => AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName.Replace("+", ".") == type);
|
||||
static readonly Func<string, Type?> typeLocator = type => AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(t =>
|
||||
{
|
||||
var fullName = t.FullName;
|
||||
return fullName != null && fullName.Replace("+", ".", StringComparison.Ordinal) == type;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Filters using the specified filter descriptors.
|
||||
@@ -18,28 +23,30 @@ namespace System.Linq.Dynamic.Core
|
||||
public static IQueryable<T> Where<T>(
|
||||
this IQueryable<T> source,
|
||||
string predicate,
|
||||
object[] parameters = null, object[] otherParameters = null)
|
||||
object[]? parameters = null, object[]? otherParameters = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
try
|
||||
{
|
||||
if (parameters != null && !string.IsNullOrEmpty(predicate))
|
||||
{
|
||||
predicate = Regex.Replace(predicate, @"@(\d+)", match =>
|
||||
{
|
||||
int index = int.Parse(match.Groups[1].Value);
|
||||
int index = int.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (index >= parameters.Length)
|
||||
throw new InvalidOperationException($"No parameter provided for {match.Value}");
|
||||
|
||||
return ExpressionSerializer.FormatValue(parameters[index]);
|
||||
return ExpressionSerializer.FormatValue(parameters[index]) ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
predicate = (predicate == "true" ? "" : predicate)
|
||||
.Replace("DateTime(", "DateTime.Parse(")
|
||||
.Replace("DateTimeOffset(", "DateTimeOffset.Parse(")
|
||||
.Replace("DateOnly(", "DateOnly.Parse(")
|
||||
.Replace("Guid(", "Guid.Parse(")
|
||||
.Replace(" = ", " == ");
|
||||
predicate = (predicate == "true" ? "" : predicate ?? string.Empty)
|
||||
.Replace("DateTime(", "DateTime.Parse(", StringComparison.Ordinal)
|
||||
.Replace("DateTimeOffset(", "DateTimeOffset.Parse(", StringComparison.Ordinal)
|
||||
.Replace("DateOnly(", "DateOnly.Parse(", StringComparison.Ordinal)
|
||||
.Replace("Guid(", "Guid.Parse(", StringComparison.Ordinal)
|
||||
.Replace(" = ", " == ", StringComparison.Ordinal);
|
||||
|
||||
return !string.IsNullOrEmpty(predicate) ?
|
||||
source.Where(ExpressionParser.ParsePredicate<T>(predicate, typeLocator)) : source;
|
||||
@@ -56,8 +63,10 @@ namespace System.Linq.Dynamic.Core
|
||||
public static IOrderedQueryable<T> OrderBy<T>(
|
||||
this IQueryable<T> source,
|
||||
string selector,
|
||||
object[] parameters = null)
|
||||
object[]? parameters = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
try
|
||||
{
|
||||
return QueryableExtension.OrderBy(source, selector);
|
||||
@@ -71,8 +80,10 @@ namespace System.Linq.Dynamic.Core
|
||||
/// <summary>
|
||||
/// Projects each element of a sequence into a collection of property values.
|
||||
/// </summary>
|
||||
public static IQueryable Select<T>(this IQueryable<T> source, string selector, object[] parameters = null)
|
||||
public static IQueryable Select<T>(this IQueryable<T> source, string selector, object[]? parameters = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
if (source.ElementType == typeof(object))
|
||||
{
|
||||
var elementType = source.ElementType;
|
||||
@@ -95,8 +106,10 @@ namespace System.Linq.Dynamic.Core
|
||||
/// <summary>
|
||||
/// Projects each element of a sequence into a collection of property values.
|
||||
/// </summary>
|
||||
public static IQueryable Select(this IQueryable source, string selector, object[] parameters = null)
|
||||
public static IQueryable Select(this IQueryable source, string selector, object[]? parameters = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
return source.Select(selector, expression => ExpressionParser.ParseLambda(expression, source.ElementType));
|
||||
}
|
||||
|
||||
@@ -109,18 +122,27 @@ namespace System.Linq.Dynamic.Core
|
||||
return source;
|
||||
}
|
||||
|
||||
if (!selector.Contains("=>"))
|
||||
if (!selector.Contains("=>", StringComparison.Ordinal))
|
||||
{
|
||||
var properties = selector
|
||||
.Replace("new (", "").Replace(")", "").Replace("new {", "").Replace("}", "").Trim()
|
||||
.Replace("new (", "", StringComparison.Ordinal).Replace(")", "", StringComparison.Ordinal).Replace("new {", "", StringComparison.Ordinal).Replace("}", "", StringComparison.Ordinal).Trim()
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
selector = string.Join(", ", properties
|
||||
.Select(s => (s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim().Replace(".", "_") : s.Trim().Replace(".", "_")) +
|
||||
" = " + $"it.{s.Split(" as ").FirstOrDefault().Replace(".", "?.").Trim()}"));
|
||||
.Select(s =>
|
||||
{
|
||||
var parts = s.Split(" as ", StringSplitOptions.RemoveEmptyEntries);
|
||||
var sourcePart = (parts.FirstOrDefault() ?? s).Trim();
|
||||
var targetPart = (parts.Length > 1 ? parts.Last() : sourcePart).Trim();
|
||||
|
||||
var safeTarget = targetPart.Replace(".", "_", StringComparison.Ordinal);
|
||||
var safeSource = sourcePart.Replace(".", "?.", StringComparison.Ordinal);
|
||||
|
||||
return $"{safeTarget} = it.{safeSource}";
|
||||
}));
|
||||
}
|
||||
|
||||
var lambda = lambdaCreator(selector.Contains("=>") ? selector : $"it => new {{ {selector} }}");
|
||||
var lambda = lambdaCreator(selector.Contains("=>", StringComparison.Ordinal) ? selector : $"it => new {{ {selector} }}");
|
||||
|
||||
return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), nameof(Queryable.Select),
|
||||
[source.ElementType, lambda.Body.Type], source.Expression, Expression.Quote(lambda)));
|
||||
|
||||
@@ -39,7 +39,7 @@ static class DynamicTypeFactory
|
||||
"set_" + propertyNames[i],
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
null,
|
||||
[propertyTypes[i]]);
|
||||
new[] { propertyTypes[i] });
|
||||
|
||||
var setterIl = setterMethod.GetILGenerator();
|
||||
setterIl.Emit(OpCodes.Ldarg_0);
|
||||
@@ -51,6 +51,10 @@ static class DynamicTypeFactory
|
||||
}
|
||||
|
||||
var dynamicType = typeBuilder.CreateType();
|
||||
if (dynamicType == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create dynamic type.");
|
||||
}
|
||||
return dynamicType;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
class Token
|
||||
{
|
||||
public TokenType Type { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public ValueKind ValueKind { get; set; } = ValueKind.None;
|
||||
public int IntValue { get; internal set; }
|
||||
public uint UintValue { get; internal set; }
|
||||
public long LongValue { get; internal set; }
|
||||
public ulong UlongValue { get; internal set; }
|
||||
public decimal DecimalValue { get; internal set; }
|
||||
public float FloatValue { get; internal set; }
|
||||
public double DoubleValue { get; internal set; }
|
||||
|
||||
public Token(TokenType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public Token(TokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public ConstantExpression ToConstantExpression()
|
||||
{
|
||||
return ValueKind switch
|
||||
{
|
||||
ValueKind.Null => Expression.Constant(null),
|
||||
ValueKind.String => Expression.Constant(Value),
|
||||
ValueKind.Character => Expression.Constant(Value[0]),
|
||||
ValueKind.Int => Expression.Constant(IntValue),
|
||||
ValueKind.UInt => Expression.Constant(UintValue),
|
||||
ValueKind.Long => Expression.Constant(LongValue),
|
||||
ValueKind.ULong => Expression.Constant(UlongValue),
|
||||
ValueKind.Float => Expression.Constant(FloatValue),
|
||||
ValueKind.Double => Expression.Constant(DoubleValue),
|
||||
ValueKind.Decimal => Expression.Constant(DecimalValue),
|
||||
ValueKind.True => Expression.Constant(true),
|
||||
ValueKind.False => Expression.Constant(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported value kind: {ValueKind}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ValueKind
|
||||
{
|
||||
None,
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Double,
|
||||
Decimal,
|
||||
Character,
|
||||
Null,
|
||||
True,
|
||||
False,
|
||||
Long,
|
||||
UInt,
|
||||
ULong,
|
||||
}
|
||||
|
||||
|
||||
enum TokenType
|
||||
{
|
||||
None,
|
||||
Identifier,
|
||||
EqualsEquals,
|
||||
NotEquals,
|
||||
EqualsGreaterThan,
|
||||
StringLiteral,
|
||||
NumericLiteral,
|
||||
Dot,
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
Comma,
|
||||
AmpersandAmpersand,
|
||||
Ampersand,
|
||||
BarBar,
|
||||
Bar,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThanOrEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
CharacterLiteral,
|
||||
QuestionMark,
|
||||
QuestionMarkQuestionMark,
|
||||
Colon,
|
||||
QuestionDot,
|
||||
New,
|
||||
NullLiteral,
|
||||
TrueLiteral,
|
||||
FalseLiteral,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
ExclamationMark,
|
||||
Equals,
|
||||
Caret,
|
||||
GreaterThanGreaterThan,
|
||||
LessThanLessThan,
|
||||
}
|
||||
|
||||
static class TokenTypeExtensions
|
||||
{
|
||||
public static ExpressionType ToExpressionType(this TokenType tokenType)
|
||||
{
|
||||
return tokenType switch
|
||||
{
|
||||
TokenType.EqualsEquals => ExpressionType.Equal,
|
||||
TokenType.NotEquals => ExpressionType.NotEqual,
|
||||
TokenType.EqualsGreaterThan => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.AmpersandAmpersand => ExpressionType.AndAlso,
|
||||
TokenType.Ampersand => ExpressionType.And,
|
||||
TokenType.BarBar => ExpressionType.OrElse,
|
||||
TokenType.Bar => ExpressionType.Or,
|
||||
TokenType.GreaterThan => ExpressionType.GreaterThan,
|
||||
TokenType.LessThan => ExpressionType.LessThan,
|
||||
TokenType.LessThanOrEqual => ExpressionType.LessThanOrEqual,
|
||||
TokenType.GreaterThanOrEqual => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.Plus => ExpressionType.Add,
|
||||
TokenType.Minus => ExpressionType.Subtract,
|
||||
TokenType.Star => ExpressionType.Multiply,
|
||||
TokenType.Slash => ExpressionType.Divide,
|
||||
TokenType.Caret => ExpressionType.ExclusiveOr,
|
||||
TokenType.GreaterThanGreaterThan => ExpressionType.RightShift,
|
||||
TokenType.LessThanLessThan => ExpressionType.LeftShift,
|
||||
_ => throw new InvalidOperationException($"Unsupported token type: {tokenType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressionLexer(string expression)
|
||||
/// <summary>
|
||||
/// Lexer for parsing expressions into tokens.
|
||||
/// </summary>
|
||||
internal class ExpressionLexer(string expression)
|
||||
{
|
||||
private int position;
|
||||
|
||||
@@ -163,7 +27,7 @@ class ExpressionLexer(string expression)
|
||||
position += count;
|
||||
}
|
||||
|
||||
bool TryAdvance(char expected)
|
||||
private bool TryAdvance(char expected)
|
||||
{
|
||||
if (Peek(1) == expected)
|
||||
{
|
||||
@@ -182,6 +46,11 @@ class ExpressionLexer(string expression)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the expression and returns a list of tokens.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to scan.</param>
|
||||
/// <returns>A list of tokens.</returns>
|
||||
public static List<Token> Scan(string expression)
|
||||
{
|
||||
var lexer = new ExpressionLexer(expression);
|
||||
@@ -189,6 +58,10 @@ class ExpressionLexer(string expression)
|
||||
return [.. lexer.Scan()];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the expression and returns an enumerable of tokens.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of tokens.</returns>
|
||||
public IEnumerable<Token> Scan()
|
||||
{
|
||||
while (position < expression.Length)
|
||||
@@ -418,7 +291,7 @@ class ExpressionLexer(string expression)
|
||||
var digit = Peek();
|
||||
|
||||
int digitValue;
|
||||
|
||||
|
||||
if (digit >= '0' && digit <= '9')
|
||||
{
|
||||
digitValue = digit - '0';
|
||||
@@ -645,7 +518,7 @@ class ExpressionLexer(string expression)
|
||||
break;
|
||||
case 'D':
|
||||
case 'd':
|
||||
hasDSuffix = true;
|
||||
hasDSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
case 'M':
|
||||
@@ -754,7 +627,7 @@ class ExpressionLexer(string expression)
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -870,4 +743,4 @@ class ExpressionLexer(string expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class ExpressionParser
|
||||
/// </summary>
|
||||
public static Expression<Func<T, TResult>> ParseLambda<T, TResult>(string expression, Func<string, Type?>? typeResolver = null)
|
||||
{
|
||||
var lambda = ParseLambda(expression, typeof(T), typeResolver);
|
||||
var lambda = ParseLambda<T>(expression, typeResolver);
|
||||
|
||||
return Expression.Lambda<Func<T, TResult>>(lambda.Body, lambda.Parameters[0]);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public class ExpressionParser
|
||||
}
|
||||
|
||||
private readonly List<Token> tokens;
|
||||
private int position = 0;
|
||||
private int position;
|
||||
private readonly Func<string, Type?>? typeResolver;
|
||||
private readonly Stack<ParameterExpression> parameterStack = new();
|
||||
|
||||
@@ -414,6 +414,11 @@ public class ExpressionParser
|
||||
return ParseStaticMemberAccess(type, parameter);
|
||||
}
|
||||
|
||||
if (TryParseQualifiedType(out var qualifiedType))
|
||||
{
|
||||
return ParseStaticMemberAccess(qualifiedType, parameter);
|
||||
}
|
||||
|
||||
if (Peek(1).Type == TokenType.OpenParen)
|
||||
{
|
||||
Advance(1);
|
||||
@@ -542,19 +547,10 @@ public class ExpressionParser
|
||||
else
|
||||
{
|
||||
Type? elementType = null;
|
||||
var nullable = false;
|
||||
|
||||
if (token.Type == TokenType.Identifier)
|
||||
if (TryParseQualifiedArrayType(out var parsedElementType))
|
||||
{
|
||||
var typeName = token.Value;
|
||||
elementType = GetWellKnownType(typeName);
|
||||
Advance(1);
|
||||
|
||||
if (Peek().Type == TokenType.QuestionMark)
|
||||
{
|
||||
nullable = true;
|
||||
Advance(1);
|
||||
}
|
||||
elementType = parsedElementType;
|
||||
}
|
||||
|
||||
Expect(TokenType.OpenBracket);
|
||||
@@ -585,11 +581,6 @@ public class ExpressionParser
|
||||
elementType = elements.Count > 0 ? elements[0].Type : typeof(object);
|
||||
}
|
||||
|
||||
if (nullable)
|
||||
{
|
||||
elementType = typeof(Nullable<>).MakeGenericType(elementType);
|
||||
}
|
||||
|
||||
return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType)));
|
||||
}
|
||||
default:
|
||||
@@ -653,7 +644,7 @@ public class ExpressionParser
|
||||
{
|
||||
var name = typeName.ToString();
|
||||
|
||||
var type = GetWellKnownType(name) ?? typeResolver?.Invoke(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
|
||||
var type = ResolveType(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
|
||||
|
||||
if (nullable && type.IsValueType)
|
||||
{
|
||||
@@ -729,6 +720,156 @@ public class ExpressionParser
|
||||
return Expression.Call(null, method, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a qualified type name (e.g. System.DateTime or System.DateTime?) and returns the resolved type.
|
||||
/// Advances position past the type name. The next token will be . for member access or [ for array.
|
||||
/// Uses backtracking to find the longest resolvable type prefix (e.g. System.DateTime in System.DateTime.SpecifyKind).
|
||||
/// </summary>
|
||||
private bool TryParseQualifiedType(out Type type)
|
||||
{
|
||||
type = null!;
|
||||
|
||||
var token = Peek();
|
||||
if (token.Type != TokenType.Identifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startPosition = position;
|
||||
var parts = new List<string> { token.Value };
|
||||
Advance(1);
|
||||
|
||||
while (Peek().Type == TokenType.Dot)
|
||||
{
|
||||
Advance(1);
|
||||
token = Peek();
|
||||
if (token.Type != TokenType.Identifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
parts.Add(token.Value);
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
var nullable = false;
|
||||
if (Peek().Type == TokenType.QuestionMark)
|
||||
{
|
||||
nullable = true;
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
for (var i = parts.Count; i >= 1; i--)
|
||||
{
|
||||
var typeName = string.Join(".", parts.Take(i));
|
||||
var resolvedType = ResolveType(typeName);
|
||||
if (resolvedType != null)
|
||||
{
|
||||
if (nullable && resolvedType.IsValueType)
|
||||
{
|
||||
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
|
||||
}
|
||||
|
||||
position = startPosition;
|
||||
var tokensToConsume = (i * 2) - 1 + (nullable ? 1 : 0);
|
||||
for (var t = 0; t < tokensToConsume; t++)
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
type = resolvedType;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
position = startPosition;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a qualified array element type (e.g. System.DateTime? or DateTime) before [].
|
||||
/// Advances position past the type name to the [. Returns the element type for the array.
|
||||
/// </summary>
|
||||
private bool TryParseQualifiedArrayType(out Type? elementType)
|
||||
{
|
||||
elementType = null;
|
||||
|
||||
var token = Peek();
|
||||
if (token.Type != TokenType.Identifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startPosition = position;
|
||||
var parts = new List<string> { token.Value };
|
||||
Advance(1);
|
||||
|
||||
while (Peek().Type == TokenType.Dot)
|
||||
{
|
||||
Advance(1);
|
||||
token = Peek();
|
||||
if (token.Type != TokenType.Identifier)
|
||||
{
|
||||
position = startPosition;
|
||||
return false;
|
||||
}
|
||||
parts.Add(token.Value);
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
var nullable = false;
|
||||
if (Peek().Type == TokenType.QuestionMark)
|
||||
{
|
||||
nullable = true;
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
if (Peek().Type != TokenType.OpenBracket)
|
||||
{
|
||||
position = startPosition;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = parts.Count; i >= 1; i--)
|
||||
{
|
||||
var typeName = string.Join(".", parts.Take(i));
|
||||
var resolvedType = ResolveType(typeName);
|
||||
if (resolvedType != null)
|
||||
{
|
||||
if (nullable && resolvedType.IsValueType)
|
||||
{
|
||||
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
|
||||
}
|
||||
elementType = resolvedType;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
position = startPosition;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a type name using well-known types, the optional type resolver, or by searching loaded assemblies.
|
||||
/// This allows any type from loaded assemblies to be resolved without hardcoding.
|
||||
/// </summary>
|
||||
private Type? ResolveType(string typeName)
|
||||
{
|
||||
return GetWellKnownType(typeName)
|
||||
?? typeResolver?.Invoke(typeName)
|
||||
?? ResolveTypeFromAssemblies(typeName);
|
||||
}
|
||||
|
||||
private static Type? ResolveTypeFromAssemblies(string typeName)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(t =>
|
||||
{
|
||||
var fullName = t.FullName;
|
||||
return fullName != null && fullName.Replace("+", ".", StringComparison.Ordinal) == typeName;
|
||||
});
|
||||
}
|
||||
|
||||
private static Type? GetWellKnownType(string typeName)
|
||||
{
|
||||
return typeName switch
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -30,6 +30,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitLambda<T>(Expression<T> node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
if (node.Parameters.Count > 1)
|
||||
{
|
||||
_sb.Append("(");
|
||||
@@ -52,6 +53,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
_sb.Append(node.Name);
|
||||
return node;
|
||||
}
|
||||
@@ -59,10 +61,11 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMember(MemberExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
if (node.Expression != null)
|
||||
{
|
||||
Visit(node.Expression);
|
||||
_sb.Append($".{node.Member.Name}");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $".{node.Member.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -74,14 +77,15 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
if (node.Method.IsStatic && node.Arguments.Count > 0 &&
|
||||
(node.Method.DeclaringType == typeof(Enumerable) ||
|
||||
(node.Method.DeclaringType == typeof(Enumerable) ||
|
||||
node.Method.DeclaringType == typeof(Queryable)))
|
||||
{
|
||||
Visit(node.Arguments[0]);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");
|
||||
|
||||
for (int i = 1; i < node.Arguments.Count; i++)
|
||||
for (int i = 1; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 1) _sb.Append(", ");
|
||||
|
||||
@@ -99,7 +103,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
}
|
||||
else if (node.Method.IsStatic)
|
||||
{
|
||||
_sb.Append($"{node.Method.DeclaringType.Name}.{node.Method.Name}(");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $"{node.Method.DeclaringType?.Name}.{node.Method.Name}(");
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
{
|
||||
@@ -114,11 +118,11 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
if (node.Object != null)
|
||||
{
|
||||
Visit(node.Object);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append($"{node.Method.Name}(");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $"{node.Method.Name}(");
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
@@ -136,17 +140,18 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitUnary(UnaryExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
if (node.NodeType == ExpressionType.Not)
|
||||
{
|
||||
_sb.Append("(!(");
|
||||
Visit(node.Operand);
|
||||
_sb.Append("))");
|
||||
}
|
||||
else if (node.NodeType == ExpressionType.Convert)
|
||||
else if (node.NodeType == ExpressionType.Convert)
|
||||
{
|
||||
if (node.Operand is IndexExpression indexExpr)
|
||||
{
|
||||
_sb.Append($"({node.Type.DisplayName(true).Replace("+",".")})");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $"({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
|
||||
|
||||
Visit(indexExpr.Object);
|
||||
|
||||
@@ -175,20 +180,18 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConstant(ConstantExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
_sb.Append(FormatValue(node.Value));
|
||||
return node;
|
||||
}
|
||||
|
||||
internal static string FormatValue(object value)
|
||||
internal static string? FormatValue(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
return "null";
|
||||
|
||||
return value switch
|
||||
{
|
||||
string s when s == string.Empty => @"""""",
|
||||
string s when s.Length == 0 => @"""""",
|
||||
null => "null",
|
||||
string s => @$"""{s.Replace("\"", "\\\"")}""",
|
||||
string s => @$"""{s.Replace("\"", "\\\"", StringComparison.Ordinal)}""",
|
||||
char c => $"'{c}'",
|
||||
bool b => b.ToString().ToLowerInvariant(),
|
||||
DateTime dt => FormatDateTime(dt),
|
||||
@@ -198,7 +201,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")",
|
||||
IEnumerable enumerable when value is not string => FormatEnumerable(enumerable),
|
||||
_ => value.GetType().IsEnum
|
||||
? $"({value.GetType().FullName.Replace("+", ".")})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
|
||||
? $"({value.GetType()?.FullName?.Replace("+", ".", StringComparison.Ordinal)})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
@@ -214,14 +217,15 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
private static string FormatEnumerable(IEnumerable enumerable)
|
||||
{
|
||||
var arrayType = enumerable.AsQueryable().ElementType;
|
||||
|
||||
|
||||
var items = enumerable.Cast<object>().Select(FormatValue);
|
||||
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".") : "")}[] {{ {string.Join(", ", items)} }}";
|
||||
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) : "")}[] {{ {string.Join(", ", items)} }}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitNewArray(NewArrayExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
|
||||
(node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);
|
||||
|
||||
@@ -245,9 +249,10 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitBinary(BinaryExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
_sb.Append("(");
|
||||
Visit(node.Left);
|
||||
_sb.Append($" {GetOperator(node.NodeType)} ");
|
||||
_sb.Append(CultureInfo.InvariantCulture, $" {GetOperator(node.NodeType)} ");
|
||||
Visit(node.Right);
|
||||
_sb.Append(")");
|
||||
return node;
|
||||
@@ -256,6 +261,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConditional(ConditionalExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
_sb.Append("(");
|
||||
Visit(node.Test);
|
||||
_sb.Append(" ? ");
|
||||
@@ -266,6 +272,21 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitDefault(DefaultExpression node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
if (!node.Type.IsValueType || Nullable.GetUnderlyingType(node.Type) != null)
|
||||
{
|
||||
_sb.Append("null");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(CultureInfo.InvariantCulture, $"default({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExpressionType to its corresponding C# operator.
|
||||
/// </summary>
|
||||
@@ -290,7 +311,7 @@ public class ExpressionSerializer : ExpressionVisitor
|
||||
ExpressionType.Coalesce => "??",
|
||||
_ => throw new NotSupportedException($"Unsupported operator: {type}")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -333,6 +354,7 @@ public static class SharedTypeExtensions
|
||||
/// <returns>A string representing the type name.</returns>
|
||||
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
var stringBuilder = new StringBuilder();
|
||||
ProcessType(stringBuilder, type, fullName, compilable);
|
||||
return stringBuilder.ToString();
|
||||
@@ -443,7 +465,7 @@ public static class SharedTypeExtensions
|
||||
}
|
||||
}
|
||||
|
||||
var genericPartIndex = type.Name.IndexOf('`');
|
||||
var genericPartIndex = type.Name.IndexOf('`', StringComparison.Ordinal);
|
||||
if (genericPartIndex <= 0)
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
@@ -18,8 +19,9 @@ namespace Radzen.Blazor
|
||||
/// <summary>
|
||||
/// Gets enum description.
|
||||
/// </summary>
|
||||
public static string GetDisplayDescription(this Enum enumValue, Func<string, string> translationFunction = null)
|
||||
public static string GetDisplayDescription(this Enum enumValue, Func<string, string>? translationFunction = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(enumValue);
|
||||
var enumValueAsString = enumValue.ToString();
|
||||
var val = enumValue.GetType().GetMember(enumValueAsString).FirstOrDefault();
|
||||
var enumVal = val?.GetCustomAttribute<DisplayAttribute>()?.GetDescription() ?? enumValueAsString;
|
||||
@@ -33,10 +35,12 @@ namespace Radzen.Blazor
|
||||
/// <summary>
|
||||
/// Converts Enum to IEnumerable of Value/Text.
|
||||
/// </summary>
|
||||
public static IEnumerable<object> EnumAsKeyValuePair(Type enumType, Func<string, string> translationFunction = null)
|
||||
public static IEnumerable<object> EnumAsKeyValuePair(Type enumType, Func<string, string>? translationFunction = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(enumType);
|
||||
|
||||
Type underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
return Enum.GetValues(enumType).Cast<Enum>().Distinct().Select(val => new { Value = Convert.ChangeType(val, underlyingType), Text = val.GetDisplayDescription(translationFunction) });
|
||||
return Enum.GetValues(enumType).Cast<Enum>().Distinct().Select(val => new { Value = Convert.ChangeType(val, underlyingType, CultureInfo.InvariantCulture), Text = val.GetDisplayDescription(translationFunction) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -67,7 +71,7 @@ namespace Radzen.Blazor
|
||||
var value = typeValue.ToString();
|
||||
value = Regex.Replace(value, "([^A-Z])([A-Z])", "$1-$2");
|
||||
return Regex.Replace(value, "([A-Z]+)([A-Z][^A-Z$])", "$1-$2")
|
||||
.Trim().ToLower();
|
||||
.Trim().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
Radzen.Blazor/FabMenuDirection.cs
Normal file
38
Radzen.Blazor/FabMenuDirection.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the direction in which a RadzenFabMenu expands its items.
|
||||
/// </summary>
|
||||
public enum FabMenuDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// The menu items expand upward from the FAB button.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// The menu items expand downward from the FAB button.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// The menu items expand to the left of the FAB button.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// The menu items expand to the right of the FAB button.
|
||||
/// </summary>
|
||||
Right,
|
||||
|
||||
/// <summary>
|
||||
/// The menu items expand to the start of the FAB button.
|
||||
/// </summary>
|
||||
Start,
|
||||
|
||||
/// <summary>
|
||||
/// The menu items expand to the end of the FAB button.
|
||||
/// </summary>
|
||||
End
|
||||
}
|
||||
|
||||
106
Radzen.Blazor/FileInfo.cs
Normal file
106
Radzen.Blazor/FileInfo.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file which the user selects for upload via <see cref="Radzen.Blazor.RadzenUpload" />.
|
||||
/// </summary>
|
||||
public class FileInfo : IBrowserFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates FileInfo.
|
||||
/// </summary>
|
||||
public FileInfo()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
private IBrowserFile? source;
|
||||
|
||||
/// <summary>
|
||||
/// Creates FileInfo with IBrowserFile as source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source browser file.</param>
|
||||
public FileInfo(IBrowserFile source)
|
||||
{
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
private string? name;
|
||||
private DateTimeOffset? lastModified;
|
||||
private string? contentType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the selected file.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return name ?? source?.Name ?? string.Empty;
|
||||
}
|
||||
set
|
||||
{
|
||||
name = value;
|
||||
}
|
||||
}
|
||||
|
||||
private long size;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size (in bytes) of the selected file.
|
||||
/// </summary>
|
||||
public long Size
|
||||
{
|
||||
get
|
||||
{
|
||||
return size != default(long) ? size : source != null ? source.Size : 0;
|
||||
}
|
||||
set
|
||||
{
|
||||
size = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IBrowserFile source.
|
||||
/// </summary>
|
||||
public IBrowserFile? Source => source;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LastModified.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastModified
|
||||
{
|
||||
get => lastModified ?? source?.LastModified ?? default;
|
||||
set => lastModified = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ContentType.
|
||||
/// </summary>
|
||||
public string ContentType
|
||||
{
|
||||
get => contentType ?? source?.ContentType ?? string.Empty;
|
||||
set => contentType = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open read stream.
|
||||
/// </summary>
|
||||
/// <param name="maxAllowedSize">The maximum allowed size.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The stream.</returns>
|
||||
public System.IO.Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
throw new InvalidOperationException("No underlying browser file is associated with this FileInfo instance.");
|
||||
}
|
||||
|
||||
return source.OpenReadStream(maxAllowedSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
18
Radzen.Blazor/FilterCaseSensitivity.cs
Normal file
18
Radzen.Blazor/FilterCaseSensitivity.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the filter case sensitivity of a component.
|
||||
/// </summary>
|
||||
public enum FilterCaseSensitivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Relies on the underlying provider (LINQ to Objects, Entity Framework etc.) to handle case sensitivity. LINQ to Objects is case sensitive. Entity Framework relies on the database collection settings.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Filters are case insensitive regardless of the underlying provider.
|
||||
/// </summary>
|
||||
CaseInsensitive
|
||||
}
|
||||
|
||||
68
Radzen.Blazor/FilterDescriptor.cs
Normal file
68
Radzen.Blazor/FilterDescriptor.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a filter in a component that supports filtering.
|
||||
/// </summary>
|
||||
public class FilterDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string? Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the property type.
|
||||
/// </summary>
|
||||
/// <value>The property type.</value>
|
||||
[JsonIgnore]
|
||||
public Type? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the filtered property.
|
||||
/// </summary>
|
||||
/// <value>The property.</value>
|
||||
public string? FilterProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to filter by.
|
||||
/// </summary>
|
||||
/// <value>The filter value.</value>
|
||||
public object? FilterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator which will compare the property value with <see cref="FilterValue" />.
|
||||
/// </summary>
|
||||
/// <value>The filter operator.</value>
|
||||
public FilterOperator FilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a second value to filter by.
|
||||
/// </summary>
|
||||
/// <value>The second filter value.</value>
|
||||
public object? SecondFilterValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the operator which will compare the property value with <see cref="SecondFilterValue" />.
|
||||
/// </summary>
|
||||
/// <value>The second filter operator.</value>
|
||||
public FilterOperator SecondFilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logic used to combine the outcome of filtering by <see cref="FilterValue" /> and <see cref="SecondFilterValue" />.
|
||||
/// </summary>
|
||||
/// <value>The logical filter operator.</value>
|
||||
public LogicalFilterOperator LogicalFilterOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mode that determines whether the filter applies to any or all items in a collection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="CollectionFilterMode"/> value indicating whether the filter is satisfied by any or all items.
|
||||
/// </value>
|
||||
public CollectionFilterMode CollectionFilterMode { get; set; }
|
||||
}
|
||||
|
||||
28
Radzen.Blazor/FilterMode.cs
Normal file
28
Radzen.Blazor/FilterMode.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the filtering mode of <see cref="Radzen.Blazor.RadzenDataGrid{TItem}" />.
|
||||
/// </summary>
|
||||
public enum FilterMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The component displays inline filtering UI and filters as you type.
|
||||
/// </summary>
|
||||
Simple,
|
||||
|
||||
/// <summary>
|
||||
/// The component displays inline filtering UI and filters as you type combined with filter operator menu.
|
||||
/// </summary>
|
||||
SimpleWithMenu,
|
||||
|
||||
/// <summary>
|
||||
/// The component displays a popup filtering UI and allows you to pick filtering operator and or filter by multiple values.
|
||||
/// </summary>
|
||||
Advanced,
|
||||
|
||||
/// <summary>
|
||||
/// The component displays a popup filtering UI and allows you to pick multiple values from list of all values.
|
||||
/// </summary>
|
||||
CheckBoxList
|
||||
}
|
||||
|
||||
93
Radzen.Blazor/FilterOperator.cs
Normal file
93
Radzen.Blazor/FilterOperator.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the comparison operator of a filter.
|
||||
/// </summary>
|
||||
public enum FilterOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// Satisfied if the current value equals the specified value.
|
||||
/// </summary>
|
||||
Equals,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value does not equal the specified value.
|
||||
/// </summary>
|
||||
NotEquals,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is less than the specified value.
|
||||
/// </summary>
|
||||
LessThan,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is less than or equal to the specified value.
|
||||
/// </summary>
|
||||
LessThanOrEquals,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is greater than the specified value.
|
||||
/// </summary>
|
||||
GreaterThan,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is greater than or equal to the specified value.
|
||||
/// </summary>
|
||||
GreaterThanOrEquals,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value contains the specified value.
|
||||
/// </summary>
|
||||
Contains,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value starts with the specified value.
|
||||
/// </summary>
|
||||
StartsWith,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value ends with the specified value.
|
||||
/// </summary>
|
||||
EndsWith,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value does not contain the specified value.
|
||||
/// </summary>
|
||||
DoesNotContain,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is in the specified value.
|
||||
/// </summary>
|
||||
In,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is not in the specified value.
|
||||
/// </summary>
|
||||
NotIn,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is null.
|
||||
/// </summary>
|
||||
IsNull,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is <see cref="string.Empty"/>.
|
||||
/// </summary>
|
||||
IsEmpty,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is not null.
|
||||
/// </summary>
|
||||
IsNotNull,
|
||||
|
||||
/// <summary>
|
||||
/// Satisfied if the current value is not <see cref="string.Empty"/>.
|
||||
/// </summary>
|
||||
IsNotEmpty,
|
||||
|
||||
/// <summary>
|
||||
/// Custom operator if not need to generate the filter.
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
23
Radzen.Blazor/FlexWrap.cs
Normal file
23
Radzen.Blazor/FlexWrap.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents whether items are forced onto one line or can wrap onto multiple lines.
|
||||
/// </summary>
|
||||
public enum FlexWrap
|
||||
{
|
||||
/// <summary>
|
||||
/// The items are laid out in a single line.
|
||||
/// </summary>
|
||||
NoWrap,
|
||||
|
||||
/// <summary>
|
||||
/// The items break into multiple lines.
|
||||
/// </summary>
|
||||
Wrap,
|
||||
|
||||
/// <summary>
|
||||
/// The items break into multiple lines reversed.
|
||||
/// </summary>
|
||||
WrapReverse
|
||||
}
|
||||
|
||||
@@ -39,8 +39,30 @@ namespace Radzen
|
||||
/// AutoCompleteType.</value>
|
||||
public virtual string AutoCompleteAttribute
|
||||
{
|
||||
get => Attributes != null && Attributes.ContainsKey("AutoComplete") && $"{Attributes["AutoComplete"]}".ToLower() == "false" ? DefaultAutoCompleteAttribute :
|
||||
Attributes != null && Attributes.ContainsKey("AutoComplete") ? Attributes["AutoComplete"] as string ?? AutoCompleteType.GetAutoCompleteValue() : AutoCompleteType.GetAutoCompleteValue();
|
||||
get
|
||||
{
|
||||
if (Attributes != null && Attributes.TryGetValue("AutoComplete", out var value))
|
||||
{
|
||||
var v = (object?)value;
|
||||
var autoCompleteValue = v switch
|
||||
{
|
||||
bool boolValue => boolValue
|
||||
? AutoCompleteType.GetAutoCompleteValue()
|
||||
: DefaultAutoCompleteAttribute,
|
||||
string stringValue when bool.TryParse(stringValue, out var boolValue) => boolValue
|
||||
? AutoCompleteType.GetAutoCompleteValue()
|
||||
: DefaultAutoCompleteAttribute,
|
||||
AutoCompleteType typeValue => typeValue.GetAutoCompleteValue(),
|
||||
_ => value != null ? value.ToString() ?? AutoCompleteType.GetAutoCompleteValue() : AutoCompleteType.GetAutoCompleteValue()
|
||||
};
|
||||
|
||||
return string.Equals(autoCompleteValue, "false", StringComparison.OrdinalIgnoreCase)
|
||||
? DefaultAutoCompleteAttribute
|
||||
: autoCompleteValue;
|
||||
}
|
||||
|
||||
return AutoCompleteType.GetAutoCompleteValue();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,7 +70,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
public virtual string DefaultAutoCompleteAttribute { get; set; } = "off";
|
||||
|
||||
object ariaAutoComplete;
|
||||
object? ariaAutoComplete;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task SetParametersAsync(ParameterView parameters)
|
||||
@@ -56,10 +78,10 @@ namespace Radzen
|
||||
parameters = parameters.TryGetValue("aria-autocomplete", out ariaAutoComplete) ?
|
||||
ParameterView.FromDictionary(parameters
|
||||
.ToDictionary().Where(i => i.Key != "aria-autocomplete").ToDictionary(i => i.Key, i => i.Value)
|
||||
.ToDictionary(i => i.Key, i => i.Value))
|
||||
.ToDictionary(i => i.Key, i => (object?)i.Value))
|
||||
: parameters;
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
await base.SetParametersAsync(parameters).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,9 +92,11 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// Gets the aria-autocomplete attribute's string value.
|
||||
/// </summary>
|
||||
public virtual string AriaAutoCompleteAttribute
|
||||
public virtual string? AriaAutoCompleteAttribute
|
||||
{
|
||||
get => AutoCompleteAttribute == DefaultAutoCompleteAttribute ? DefaultAriaAutoCompleteAttribute : ariaAutoComplete as string;
|
||||
get => string.Equals(AutoCompleteAttribute, DefaultAutoCompleteAttribute, StringComparison.Ordinal)
|
||||
? DefaultAriaAutoCompleteAttribute
|
||||
: ariaAutoComplete as string;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -96,7 +120,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The component name.</value>
|
||||
[Parameter]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tab order index for keyboard navigation.
|
||||
@@ -112,7 +136,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The placeholder.</value>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; }
|
||||
public string? Placeholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="FormComponent{T}"/> is disabled.
|
||||
@@ -124,21 +148,21 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The form
|
||||
/// </summary>
|
||||
IRadzenForm _form;
|
||||
IRadzenForm? _form;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the edit context.
|
||||
/// </summary>
|
||||
/// <value>The edit context.</value>
|
||||
[CascadingParameter]
|
||||
public EditContext EditContext { get; set; }
|
||||
public EditContext? EditContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the form.
|
||||
/// </summary>
|
||||
/// <value>The form.</value>
|
||||
[CascadingParameter]
|
||||
public IRadzenForm Form
|
||||
public IRadzenForm? Form
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -189,14 +213,14 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// The value
|
||||
/// </summary>
|
||||
protected T _value;
|
||||
protected T? _value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
/// <value>The value.</value>
|
||||
[Parameter]
|
||||
public virtual T Value
|
||||
public virtual T? Value
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -230,7 +254,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <value>The value expression.</value>
|
||||
[Parameter]
|
||||
public Expression<Func<T>> ValueExpression { get; set; }
|
||||
public Expression<Func<T>>? ValueExpression { get; set; }
|
||||
/// <summary>
|
||||
/// Sets the parameters asynchronous.
|
||||
/// </summary>
|
||||
@@ -262,7 +286,7 @@ namespace Radzen
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="ValidationStateChangedEventArgs"/> instance containing the event data.</param>
|
||||
private void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
|
||||
private void ValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -280,19 +304,21 @@ namespace Radzen
|
||||
}
|
||||
|
||||
Form?.RemoveComponent(this);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object GetValue()
|
||||
public object? GetValue()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="E:ContextMenu" /> event.
|
||||
/// Handles the ContextMenu event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="MouseEventArgs"/> instance containing the event data.</param>
|
||||
/// <returns>Task.</returns>
|
||||
@@ -318,10 +344,10 @@ namespace Radzen
|
||||
|
||||
/// <summary> Provides support for RadzenFormField integration. </summary>
|
||||
[CascadingParameter]
|
||||
public IFormFieldContext FormFieldContext { get; set; }
|
||||
public IFormFieldContext? FormFieldContext { get; set; }
|
||||
|
||||
/// <summary> Gets the current placeholder. Returns empty string if this component is inside a RadzenFormField.</summary>
|
||||
protected string CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
protected string? CurrentPlaceholder => FormFieldContext?.AllowFloatingLabel == true ? " " : Placeholder;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async ValueTask FocusAsync()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user