Compare commits

..

239 Commits

Author SHA1 Message Date
Vladimir Enchev
7ae64ba919 Version updated 2026-02-12 12:22:54 +02:00
Vladimir Enchev
6e94a7c65b DataGrid column defaults lost on clear filter 2026-02-12 12:22:18 +02:00
yordanov
44e20b2b5f Update premium themes 2026-02-12 11:49:41 +02:00
Atanas Korchev
d109c2e295 Remove NET7 preprocessor guards from Blazor components.
Assume .NET 7+ paths by default and delete legacy fallback-only event handler declarations.
2026-02-12 11:22:24 +02:00
yordanov
0feffad278 Fix secondary text button colors in dark themes 2026-02-12 11:20:39 +02:00
Vladimir Enchev
3be412643f Revert "DataGrid column initialization logic improved"
This reverts commit 547a681878.
2026-02-12 11:10:01 +02:00
Atanas Korchev
6d8c4ceb16 Remove NET6-specific conditions from Blazor library sources.
Drop net6 package references and inline NET6_0_OR_GREATER code paths now that .NET 6 support is no longer targeted.
2026-02-12 10:46:45 +02:00
Atanas Korchev
40b7d84224 Remove .NET 6 instructions from onboarding demo pages.
Keep tab navigation stable by defaulting to the first tab when legacy version routes are requested.
2026-02-12 10:46:45 +02:00
Atanas Korchev
4511c654f9 Normalize demo routes and enforce canonical URL redirects.
Use middleware to permanently redirect legacy/docs aliases and trailing-slash URLs to canonical paths, and keep only canonical @page directives on demo pages to prevent duplicate content and route drift.
2026-02-12 10:46:45 +02:00
Vladimir Enchev
547a681878 DataGrid column initialization logic improved 2026-02-12 10:38:46 +02:00
yordanov
cad80d533d Fix RadzenLogin tests 2026-02-12 10:29:50 +02:00
yordanov
f485420ba9 Fix RadzenLink sizing and RadzenLogin button styles 2026-02-12 10:23:33 +02:00
Vladimir Enchev
ac61235a48 ExpressionParser support for qualified array type fixed 2026-02-12 09:34:32 +02:00
Vladimir Enchev
61bd1db8b9 More possible DataGrid memory leaks fixed 2026-02-11 22:41:37 +02:00
Vladimir Enchev
5992a97ef9 RadzenDropDownDataGrid will not search as you type
Fix #2444
2026-02-11 22:31:08 +02:00
Vladimir Enchev
157070c922 DropDownBase selection not cleared in some cases 2026-02-11 22:03:10 +02:00
Vladimir Enchev
14e39f665f Version updated 2026-02-11 11:36:55 +02:00
Vladimir Enchev
2f77a0b849 Fixed Invalid operation exception with complex filter queries
Expression.Default support added to ExpressionSerializer
2026-02-11 11:36:35 +02:00
Vladimir Enchev
c3af020b81 Tooltip position in RTL mode improved 2026-02-11 11:16:53 +02:00
Vladimir Enchev
19460b899b Fix to possible DotNetObjectReference instance was already disposed 2026-02-11 10:50:41 +02:00
Vladimir Enchev
3cf12c82bf Version updated 2026-02-10 18:09:08 +02:00
Vladimir Enchev
adf86b2896 DataGrid CheckBoxList Filter crashes page in Firefox
Fixed null value for attributes and edge cases in Blazor HTML update under FF #2442
2026-02-10 18:08:28 +02:00
Atanas Korchev
7b96fe3cb4 Fix llms.txt generator: preserve @code blocks, fix @page regex, remove noise
- Add \b word boundary to directive regex so @page no longer matches
  @pageSizeOptions, @pageIndex etc., which was stripping attribute values
  and leaving broken Razor markup in the output.
- Stop stripping @code blocks from example files — they contain the C#
  code (methods, fields, event handlers) needed to understand examples.
- Exclude AccessibilityPage, GetStarted, and ThemeServicePage from
  generation (generic setup/WCAG content, not component documentation).
- Skip "Keyboard Navigation" sections (shortcuts rendered from C# data
  that the generator cannot extract, leaving empty placeholders).
- Skip "Radzen Blazor Studio" sections (IDE-specific, not component API).
2026-02-09 20:17:33 +02:00
Atanas Korchev
9a58c3a35a Exclude sub-component pages from llms.txt sections
Sub-component .razor files without @page directives were getting
standalone sections with rendered placeholder text instead of being
embedded as code snippets via <RadzenExample> in their parent pages.
2026-02-09 17:35:55 +02:00
Atanas Korchev
876c248a8e Cleanup llms.txt from marketing content and other noise. 2026-02-09 17:12:48 +02:00
Vladimir Enchev
0ce1c06178 Version updated 2026-02-09 15:44:41 +02:00
Vladimir Enchev
16f4f1839a Fixed EF Core keyless entities exception (SQL View) 2026-02-09 15:44:24 +02:00
Vladimir Enchev
b5e30cbfe0 changelog update with v9 rendering changes 2026-02-09 10:34:46 +02:00
Vladimir Enchev
604fdfb28a net6.0 support removed 2026-02-09 10:34:27 +02:00
Vladimir Enchev
d5b40d5e9b Version updated 2026-02-09 09:56:38 +02:00
yordanov
13e1a55ee8 Update premium themes 2026-02-09 09:52:09 +02:00
Vladimir Enchev
28e9090d45 Various accessibility issues fixes (#2423)
* accessibility issues fixes

Various localizable accessibility related properties added

tests updated

side dialog close button id fixed

more accessibility improvements

tests fixed

more accessibility fixes

missing ARIA attributes added

* Add rz-hidden-accessible styles

* Update RadzenRating styles

* RadzenSwitch fixed

* RadzenFabMenu fixed

* tests updated

* Update expand/collapse button styles in RadzenDataGrid

* Fix responsive pager styles

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2026-02-09 09:25:34 +02:00
Vladimir Enchev
d03020062e Various possible memory leaks fixed 2026-02-09 09:07:29 +02:00
Atanas Korchev
fc6fe54635 Fix 302 redirect during SSR that hurt SEO for clean URLs
During server-side prerendering the ThemeChanged handler would call
NavigateTo which resulted in a 302 redirect (e.g. /datagrid → /datagrid?theme=material3).
This conflicted with the canonical tag pointing back to the clean URL,
confusing Google and causing ranking drops. Unsubscribe from ThemeChanged
during SSR so the redirect no longer occurs. Interactive theme switching
is unaffected since WebAssembly uses a separate scoped instance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 21:33:34 +02:00
Vladimir Enchev
505ffa7fe4 Splitter ChangeStateOnResize added 2026-02-06 12:36:29 +02:00
Vladimir Enchev
4f82ac9599 Accessing sub properties of nullable types improved 2026-02-06 10:36:53 +02:00
Vladimir Enchev
5bbf7c1fde build fixed 2026-02-04 14:46:25 +02:00
Vladimir Enchev
e416cede62 DropDownBase possible null reference exception with browser autofil 2026-02-04 12:08:41 +02:00
Vladimir Enchev
270a7e0f80 DropDown, ListBox and DropDownDatGrid paste using context menu into search box not filtering
Fix #2437
2026-02-04 11:28:12 +02:00
Vladimir Enchev
1a12a75bde Version updated 2026-02-03 09:56:04 +02:00
Vladimir Enchev
d1917eac0c Fixed QRCode eyes with transparent background 2026-02-03 08:44:24 +02:00
Vladimir Enchev
09830f0ea2 DataGrid possible memory leak fixed 2026-02-03 07:46:32 +02:00
Vladimir Enchev
d34e0684fb DataGrid GroupRowRenderEventArgs Expandable property added 2026-02-02 12:09:40 +02:00
Vladimir Enchev
482eca3278 tests fixed 2026-01-30 12:23:10 +02:00
Vladimir Enchev
5c8ac16c83 Version updated 2026-01-30 12:09:54 +02:00
Vladimir Enchev
29382cf0f4 DataGrid QueryOnlyVisibleColumns property added 2026-01-30 12:09:34 +02:00
Vladimir Enchev
53204cc8d6 RadzenPager GoToPage() will not update page index 2026-01-30 09:53:52 +02:00
Vladimir Enchev
6cf550c517 Version updated 2026-01-28 17:54:45 +02:00
vadimstrekha
7bf107af4c Fix sytax error in Radzen.Blazor.js (#2436) 2026-01-28 17:53:21 +02:00
Vladimir Enchev
adf2785a5a Version updated 2026-01-28 15:31:27 +02:00
Vladimir Enchev
cae44df00a Bar Charts have a zero for the min and max on the y-axis
Fix #2434
2026-01-28 15:31:05 +02:00
Vladimir Enchev
8ba1c69573 RadzenQRCode and RadzenBarcode ToSvg() methods added 2026-01-28 10:31:13 +02:00
Vladimir Enchev
56031c2fd4 QRCode and Barcode save to SVG examples added 2026-01-28 10:00:24 +02:00
Vladimir Enchev
ad44802d30 RadzenBarcodeEncoder and RadzenQREncoder made public
Fix #2433
2026-01-27 18:27:40 +02:00
Vladimir Enchev
64ca088e61 ListBox duplicates the first typed letter in WASM when inside Popup
Fix #2429
2026-01-27 14:08:46 +02:00
Theronguard
f4777565a2 Changed the method signature to virtual, to allow overrding Enum translation with component activators (#2432) 2026-01-27 13:14:28 +02:00
Vladimir Enchev
eb1423e757 Version update 2026-01-27 08:34:44 +02:00
Vladimir Enchev
596b251511 DataGrid column custom filter indicator is active even when no filter 2026-01-27 08:34:29 +02:00
Vladimir Enchev
e186315935 DataGrid grouping arrows do not show expanded state
Fix #2431
2026-01-27 08:27:26 +02:00
yordanov
cba9a5120d Reorder examples 2026-01-26 16:14:07 +02:00
Vladimir Enchev
ee62a21ab6 version updated 2026-01-26 14:36:48 +02:00
yordanov
0a5e318f80 Update premium themes 2026-01-26 14:34:01 +02:00
Vladimir Enchev
8dd7d7f521 DataGrid simple string filter clear button not shown 2026-01-26 10:34:05 +02:00
Vladimir Enchev
69573b2d7d demo updated 2026-01-26 10:06:23 +02:00
Vladimir Enchev
5b933c6643 DataGrid reorder column stick to mouse 2026-01-23 12:11:44 +02:00
Atanas Korchev
126b2d1efa RadzenSpiderChart (#2417)
* spider chart added

* code improved

* EventConsole added

* Pastel is the default color scheme

* Fix color schemes in Spider chart

* Update SpiderChart styles

---------

Co-authored-by: Ehab Hussein <me@ehabhussein.com>
Co-authored-by: Vladimir Enchev <vladimir.enchev@gmail.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2026-01-22 14:27:01 +02:00
Vladimir Enchev
a65c1a9482 version updated 2026-01-21 17:31:55 +02:00
Vladimir Enchev
4939e8498a OData filtering by no string columns fixed 2026-01-21 17:31:33 +02:00
Vladimir Enchev
e380467853 Version updated 2026-01-21 10:17:06 +02:00
Vladimir Enchev
d3adc9733b PivotDataGrid user interaction breaks multi-column/row sorting
Fix #2427
2026-01-20 13:37:13 +02:00
Vladimir Enchev
a03fc50ee8 DataFilter should set filter Type on property change 2026-01-20 09:31:07 +02:00
Mason Voxland
c9201bf947 Fix TextProperty documentation in RadzenScheduler (#2426)
Should be string, not DateTime
2026-01-20 09:17:23 +02:00
yordanov
8c8d288afd Fix #2188 rz-layout height should be 100dvh by default 2026-01-19 17:15:40 +02:00
Vladimir Enchev
1a679af008 Chart demo source code fixed 2026-01-19 16:00:26 +02:00
yordanov
bc1654a405 Fix #2218 - RadzenSidebar border should take into account the sidebar's position 2026-01-19 13:12:54 +02:00
Atanas Korchev
94ef62e00b Customize split button dropdown icon demo is showing wrong source code. 2026-01-19 11:33:24 +02:00
Vladimir Enchev
13afd0fddb Version updated 2026-01-19 10:43:13 +02:00
Vladimir Enchev
fa7120571d DataGrid, DataFilter and PivotDataGrid numeric filter cannot be cleared 2026-01-19 10:42:35 +02:00
Vladimir Enchev
4d4e882dcc Upload API example fixed 2026-01-19 09:39:50 +02:00
Vladimir Enchev
af8c7179d1 Pivot Grid Sorting: SortOrder="SortOrder.Descending" can break sort controls
Fix #2425
2026-01-19 09:31:31 +02:00
Vladimir Enchev
c743054184 RadzenAIChat sometimes first character not detected in chat input field
Fix #2424
2026-01-19 09:17:31 +02:00
Vladimir Enchev
935d850296 Version updated 2026-01-16 11:46:46 +02:00
Vladimir Enchev
ad6b6886f1 ToFilterString() and ToODataString() fixed for non string values 2026-01-16 09:33:21 +02:00
Maxim Becker
d5d08c89de Fix filtering in RadzenAutoComplete if Data is simple list of strings (#2422) 2026-01-16 09:22:46 +02:00
Vladimir Enchev
5a246f732e Version updated 2026-01-15 18:28:58 +02:00
Vladimir Enchev
669ad421d9 Chart 0 at beginning and end of x-axis 2026-01-15 18:28:39 +02:00
Vladimir Enchev
98e8393d14 DataGrid column UniqueID initialization fixed 2026-01-15 18:08:38 +02:00
tharreck
de8dfb7e93 Fix issue where selectedItem is not updated when value is set to null in dropdown. (#2420)
Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2026-01-15 17:14:16 +02:00
Vladimir Enchev
61b32d6490 RadzenDropDown with AllowFiltering and single selection should close on ENTER 2026-01-15 09:21:08 +02:00
Vladimir Enchev
5057fb1cc8 PivotDataGrid row/col groups should respect sorting
Fix #2413
2026-01-14 16:07:14 +02:00
Vladimir Enchev
ed49dfc2fa DatePicke invalid cast with nullable DateTimeOffset 2026-01-14 13:52:29 +02:00
Vladimir Enchev
839018795c version updated 2026-01-13 14:28:29 +02:00
yordanov
c541f9d3e5 Style borders demos 2026-01-13 14:11:02 +02:00
yordanov
5e5957e4c4 Add video to borders' demos 2026-01-13 14:05:46 +02:00
yordanov
84a759267e Style Barcode demo 2026-01-13 13:48:58 +02:00
Vladimir Enchev
eb1e6fab0d Barcode added (#2416)
demo improved

various fixes

more fixes
2026-01-13 10:44:09 +02:00
Vladimir Enchev
6094953009 DataGrid drag to group or reorder restricted over grid only 2026-01-13 10:41:01 +02:00
Vladimir Enchev
b845c73ad6 DataGrid cannot group by column in some cases 2026-01-13 10:00:58 +02:00
Vladimir Enchev
2788606105 Version updated 2026-01-12 17:09:07 +02:00
Vladimir Enchev
aebf267320 Scheduler Planner/Year view: appointments missing when start month is January
Fix #2415
2026-01-12 17:08:37 +02:00
Vladimir Enchev
33896ea8db RadzenChat user is typing option added
Fix #2376
2026-01-12 10:51:11 +02:00
Vladimir Enchev
cb8699f315 RadzenTabs aria-selected accessibility issue 2026-01-12 09:50:09 +02:00
Vladimir Enchev
2a43db6560 RadzenChat always eats first key press after sending first message 2026-01-12 09:37:35 +02:00
Pavlo Iatsiuk
0ad1200870 #2406 - Allow to define Http method for Upload component (#2407)
* #2406 - Allow to define Http method for Upload component

* #2406 - Allow to define Http method for Upload component and stream raw file data

* #2406 - Allow to define Http method for Upload component
Example how to stream file

---------

Co-authored-by: Pavlo Iatsiuk <pavlo.iatsiuk@nems.eco>
2026-01-12 09:22:15 +02:00
yordanov
2debdbfd38 Update copyright year 2026-01-11 13:20:15 +02:00
tharreck
07c96bd4bd fix strange oninput behavior on textbox and textarea (server rendering) (#2410)
* use bind:get and bind:set to fix  strange behavior on input while using server rendering

* only update the Immediate textarea and textbox

---------

Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2026-01-09 14:29:03 +02:00
Vladimir Enchev
704c6abe7c Version updated 2026-01-09 08:09:02 +02:00
Vladimir Enchev
e0e0e608e2 DataGrid ToFilterString() fixed
Fix #2411
2026-01-09 08:07:17 +02:00
Vladimir Enchev
40e15a0720 Symbols package fixed 2026-01-08 10:48:34 +02:00
Vladimir Enchev
d6235fd147 Version updated 2026-01-08 10:24:26 +02:00
Vladimir Enchev
c5a3a911e0 DataGrid filtering by IsNull/IsNotNull fixed 2026-01-08 10:08:54 +02:00
Vladimir Enchev
4e7a037bc5 RadzenPickList selection by ValueProperty fixed 2026-01-08 10:08:29 +02:00
Vladimir Enchev
f1c3f46ad7 RadzenPickList ValueProperty added 2026-01-08 09:22:07 +02:00
Vladimir Enchev
6c0e39da20 DataGrid filter operators should check if the property is nullable 2026-01-07 13:48:59 +02:00
Vladimir Enchev
2a9d638acf null item selection restored 2026-01-07 13:23:04 +02:00
Vladimir Enchev
426e1ba8e8 DropDownBase/ListBox/DataGridHeaderCell selection of null item fixed 2026-01-07 13:03:07 +02:00
Vladimir Enchev
1bb36ef207 demo fixed 2026-01-07 10:06:45 +02:00
Vladimir Enchev
67cde0fd59 RadzenDropDownDataGrid paging fixed 2026-01-07 09:50:50 +02:00
Vladimir Enchev
0696cd20d5 Various warnings resolved and TreatWarningsAsErrors enabled (#2409) 2026-01-07 09:29:58 +02:00
Vladimir Enchev
34fce5188f DataGrid should not call Count() more than once 2026-01-07 09:11:33 +02:00
Vladimir Enchev
273ba0381f Version updated 2026-01-06 10:45:51 +02:00
Vladimir Enchev
63a05d86e8 Nested DataGrid ContextMenu not working properly 2026-01-06 10:41:52 +02:00
Vladimir Enchev
283e115d0a Upload file cannot be reselected after removed 2026-01-06 10:08:40 +02:00
Atanas Korchev
b513ebba8e Allow HEAD requests to routable URLs. 2026-01-05 11:41:22 +02:00
Vladimir Enchev
e324cea7b9 DropDown will select item on ENTER if there is only one item 2025-12-27 16:47:01 +02:00
Vladimir Enchev
c3cda91a2d DataGrid columns visibility not saved in settings when changing PageSize 2025-12-23 10:35:49 +02:00
Vladimir Enchev
bb9f611629 ClearSearchAfterSelection does not reset filtering after reopening DropDown
Fix #2405
2025-12-23 09:57:24 +02:00
Atanas Korchev
6cc8e62b15 Update the bug report issue template to include playground snippet instructions. 2025-12-22 14:46:27 +02:00
Atanas Korchev
5a44b76adb Use unique image name to produce a new deploy every time. 2025-12-22 12:45:07 +02:00
Atanas Korchev
109d6bc617 Update deploy.yml 2025-12-22 12:22:06 +02:00
Vladimir Enchev
7249ff6107 Version updated 2025-12-22 11:38:43 +02:00
Vladimir Enchev
406830fa13 Numeric fails to accept pasted input when Min is specified
Fix #2401
2025-12-22 10:25:17 +02:00
Vladimir Enchev
a6a3278443 Single selection filtered dropdown popup does not close when selecting an item with enter key
Fix #2398
2025-12-22 09:43:17 +02:00
Atanas Korchev
9fd8531529 Using ___ or *** in markdown content causes exception in RadzenMarkdown. Fixes #2402. 2025-12-22 08:29:07 +02:00
Atanas Korchev
c687976796 Update deploy.yml 2025-12-20 06:49:59 +02:00
Atanas Korchev
ae511929f7 Keep only one container version (the latest) 2025-12-20 06:30:41 +02:00
Atanas Korchev
e78e756206 Workaround for https://github.com/dotnet/aspnetcore/issues/64693. 2025-12-19 20:46:55 +02:00
Atanas Korchev
2e9e0dac1a Update deploy.yml 2025-12-19 19:44:36 +02:00
Atanas Korchev
bff97712e2 Update deploy.yml 2025-12-19 19:29:28 +02:00
Atanas Korchev
78bc25cce0 Update deploy.yml 2025-12-19 19:17:34 +02:00
Atanas Korchev
a2e829417d Update deploy.yml 2025-12-19 18:55:44 +02:00
Atanas Korchev
9902413daf Logging. 2025-12-19 18:54:05 +02:00
Atanas Korchev
06a35f4a73 Create deploy.yml 2025-12-19 18:26:49 +02:00
Atanas Korchev
e2a46157b9 Playground (#2400)
* Playground page.

* Persist the changes.

* Add copy button.

* Style Playground

* Update Playground button in CodeViewer

* Add AntiForgery support.

* Update Save alert

* Extract common code.

* Loading tweaks.

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-19 17:36:32 +02:00
Atanas Korchev
c4913a94c4 Remove duplicate anchors from the chart series demo. 2025-12-19 11:29:47 +02:00
Vladimir Enchev
d6e04c3ae8 DropDownDataGrid ContextMenuDataGrid added 2025-12-18 16:04:02 +02:00
Vladimir Enchev
5b17ef5217 Version updated 2025-12-17 16:19:05 +02:00
Anspitzen
8edeedc32c Rework paste handler and add same function to new drop handler (#2394)
* Rework paste handler and add same function to new drop handler

* Remove eventlistener on destroy

* Try restore formating of Radzen.Balzor.js

* Fix missing delegate flag on create editor from restoring formating

* Handle drop and paste with same c# EventCallback OnPaste/Paste
2025-12-17 11:34:04 +02:00
Atanas Korchev
543dbc9e50 Serve uploaded images as static assets. 2025-12-17 11:18:30 +02:00
tharreck
3710f4297b Add ExpandAll/CollapseAll button in hierarchy datagrid (#2391)
* Add ExpandAll/CollapseAll button in hierarchy datagrid

* update icons of ExpandAll/CollapseAll button

* Update naming and use default values for ExpandAllTitle and CollapseAllTitle

---------

Co-authored-by: Steven Torrelle <steven.torrelle@uzgent.be>
2025-12-17 08:44:30 +02:00
Paul Ruston
91d6ba4da8 Add another condition for overlapping appointments (#2392) 2025-12-16 13:56:18 +02:00
Atanas Korchev
fbfa3c1abe Update README.md 2025-12-16 13:50:54 +02:00
Atanas Korchev
d13d7bef0c RadzenMarkdown allows unsafe protocols in attributes - javascript: etc. 2025-12-16 13:45:54 +02:00
joriverm
510f5d7190 RadzenDropdown: correctly close popup on item selection, ignoring OpenOnFocus (#2389)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-12-15 16:08:29 +02:00
Vladimir Enchev
595b98d4b7 DropDownDataGrid row not focused on select 2025-12-15 11:01:29 +02:00
Atanas Korchev
c696107aeb Build does not finish. 2025-12-11 18:18:16 +02:00
Atanas Korchev
632722cd7b Llms.txt fixes. 2025-12-11 17:54:58 +02:00
Atanas Korchev
60c264ea49 Remove UseStaticAssets to restore compression. 2025-12-11 17:10:19 +02:00
Vladimir Enchev
10ce92264d Version updated 2025-12-11 10:35:03 +02:00
Vladimir Enchev
2e5c7aa6bc Mask and Numeric Immediate property added
Fix #2384, #2386
2025-12-11 10:15:24 +02:00
yordanov
1e7e2c5b51 Add video to RadzenIcon demos 2025-12-11 10:08:29 +02:00
Atanas Korchev
46790ce3fe Html editor dropdown aria attributes. Closes #2385. (#2387)
* Add ARIA attributes to HtmlEditorDropdown.

* Fix spacing and font size of rz-html-editor-dropdown

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-10 11:31:33 +02:00
yordanov
7bd891f8f0 Add video to RadzenText demos 2025-12-10 09:50:27 +02:00
Vladimir Enchev
1ae16acbac Fixed possible DropDown NullReferenceException
Fix #2380
2025-12-10 08:20:33 +02:00
Vladimir Enchev
2281fb7f61 DataGrid expand/collapse of items using right/left keys added 2025-12-10 08:12:10 +02:00
Vladimir Enchev
89e8db6c6e Version updated 2025-12-09 10:24:30 +02:00
Vladimir Enchev
11300692e5 DropDownBase search by key enabled when bound without TextProperty 2025-12-09 10:21:38 +02:00
Barry
a31161c6d8 Fix: TreeItem Template click does not toggle Checkbox (#2374)
* add click handler for treeviewitem when Template used with checkboxes

* change approach to expose a toggle option

* remove unused method and add select to toggle checkbox selection

* rename to toggle checked

---------

Co-authored-by: Barry Wright <ace90210ace@msn.com>
2025-12-09 09:02:38 +02:00
xsainteer
edd85f8ec0 added UseDisplayName parameter to DataGridColumn component (#2378) 2025-12-08 08:48:18 +02:00
Vladimir Enchev
0d420604dd code fixed 2025-12-05 12:03:43 +02:00
Vladimir Enchev
777f5666e2 Docker improved to avoid running demos browse problems 2025-12-05 11:59:42 +02:00
Vladimir Enchev
e7bded641f Version updated 2025-12-05 11:16:11 +02:00
Vladimir Enchev
6f0dfbe038 Fixed cannot use ?? unparenthesized within || and && expressions 2025-12-05 11:15:57 +02:00
Vladimir Enchev
0e92eeb50e UseStaticFiles() added to enable llms.txt visibility 2025-12-05 11:06:56 +02:00
Vladimir Enchev
50b6cb879a Version updated 2025-12-05 10:14:58 +02:00
Vladimir Enchev
739ebde6a8 Llmtxt generation added (#2379)
* RadzenBlazorDemos.Tools added

* GenerateLlmsTxt Condition update to Release

* GenerateLlmsTxt updated again

* code fixed

* content generation cleaned

* razor/html tags removed from example descriptions and the code is now inside codeblock

* more fixes
2025-12-05 10:14:15 +02:00
Vladimir Enchev
24ef74f16d Upload remove file throws cannot read property 'find' exception 2025-12-05 10:10:13 +02:00
Vladimir Enchev
9e0a57ca5b RadzenDropDownDataGrid ColumnResized, ColumnReordering and ColumnReordered added 2025-12-04 11:00:07 +02:00
Vladimir Enchev
feaebb6f0f Version updated 2025-12-04 10:52:15 +02:00
yordanov
9a45759414 Add rz-default-scrollbars CSS class to base themes 2025-12-04 10:49:10 +02:00
yordanov
14c5a0447b Remove Black Friday promo banner 2025-12-04 10:37:47 +02:00
yordanov
7793e03d82 Update premium themes 2025-12-03 10:27:13 +02:00
Martin Hans
f422573fcc Add resizable option to side dialog. (#2331)
* Add resizable option to side dialog.

* Fix resize bar CSS class typo and make sideDialogResizeHandleJsModule nullable

* Add tests for resizable side dialog.

* Rename element references.

* Enable nullable refernce types for RadzenDialg.razor.cs

* Resolve requested change

Move code back to RadzenDialog.razor.

* Use sideDialog.classList to gather position rather than data-dir attribute.

* Rework initSideDialogResize function.

- Take position from options.
- Take width and height from sideDialogs clientWidth and clientHeight.
- Take minWidth and minHeight from options.
- Remove superflous pointer capturing.
- Remove superflous 'dragging' class.

* Replace title and aria label string constants by properties.

* Treat resizableMinWidth and resizableMinHeight as present.

* Reformat

* Use options.resizableMinWidth/Height only

* No need to set min-width neither min-height on side dialog.

* Rename ResizableMinWidth => MinWidth, ResizableMinHeight => MinHeight.

* Rename initSideDialogResize => createSideDialogResizer.

* Add dialog resize bar css variables

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2025-12-03 10:22:33 +02:00
tharreck
516109ecd3 Correctly select RadzenTocItem based on scroll direction in RadzenToc (#2370)
* Select toc item based on scroll direction in TOC

* Select correct RadzenTocItem when reloading a page with anchor
cleanup code

* add debounce back into scrollhandler

---------

Co-authored-by: Torrelle Steven <steven.torrelle@uzgent.be>
2025-12-02 15:45:10 +02:00
Mason Voxland
31d32b16a5 Clicking SelectAllText in CheckBoxList will do change (#2377) 2025-12-02 11:13:16 +02:00
yordanov
435c1bd6e2 Update Black Friday promo 2025-12-01 10:56:10 +02:00
Vladimir Enchev
6d5ebf3f58 Version updated 2025-12-01 10:55:13 +02:00
Vladimir Enchev
b97036b447 Support large strings added to QRCode
Fix #2373
2025-12-01 10:51:55 +02:00
Vladimir Enchev
d7ef3cb896 Upload file not removed properly when on file delete from UI 2025-11-27 14:10:49 +02:00
Robyn Choi
2218eefa67 RadzenPivotDataGrid AddPivotAggregate exposed (#2371) 2025-11-27 08:14:10 +02:00
Vladimir Enchev
666d33d767 Version updated 2025-11-26 14:00:45 +02:00
Vladimir Enchev
3f4f83d354 PivotDataGrid filter icon popup exception after adding more fields 2025-11-26 08:35:43 +02:00
Atanas Korchev
3dea547559 Enable CSS minification for the embedded themes. 2025-11-25 18:33:46 +02:00
rklfss
0e9c6acb84 Allow Specifying the interactive SortOrder Sequence on Data Grids (#2366) 2025-11-25 15:46:23 +02:00
Vladimir Enchev
1b6881673e AI assistant messages are sent as ‘user’ instead of ‘assistant’ role. ChatMessage Role property added
Fix #2365
2025-11-25 10:17:15 +02:00
yordanov
f069b33b60 Add videos to UI fundamentals demos 2025-11-25 10:03:54 +02:00
Vladimir Enchev
c0a86e31da Version updated 2025-11-24 10:05:17 +02:00
Vladimir Enchev
7b95778efe PivotDataGrid ColumnsCollection, RowsCollection and AggregatesCollection added 2025-11-24 09:45:02 +02:00
Vladimir Enchev
ef8a102d0a DataGrid OData column Type Guid exception on set filter
Fix #2363
2025-11-24 09:18:55 +02:00
Vladimir Enchev
95448de3ed DataGrid column CloseFilter() does not work when FilterProperty has a value 2025-11-21 10:14:08 +02:00
Vladimir Enchev
0a574762c7 RadzenTocItem inherits RadzenComponentWithChildren 2025-11-18 14:10:07 +02:00
joriverm
59b1440990 RadzenTocItem: take in attributes to pass on to the list item element (#2360)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-11-18 13:56:36 +02:00
Vladimir Enchev
98d6729d4b Version updated 2025-11-18 09:06:25 +02:00
Vladimir Enchev
135a0bbe5c RadzenPivotDataGrid AddPivotField, AddPivotColumn and AddPivotRow exposed 2025-11-18 09:05:05 +02:00
Vladimir Enchev
c3f579931d RadzenDropDownDataGrid LoadChildData event added 2025-11-18 08:57:30 +02:00
Vladimir Enchev
21c51e81d2 PagedDataBoundComponent current page not synced for top and bottom pagers
Fix #2357
2025-11-18 08:48:03 +02:00
Ali Yousefi
e6538c95ad feat: keyboard key that triggers opening the popup (#2356) 2025-11-17 15:50:59 +02:00
Vladimir Enchev
0e63e87f9b DatePicker will not populate iniitially hour and minutes when bound to TimeOnly 2025-11-14 09:49:22 +02:00
Vladimir Enchev
1e900ec775 Added XML comments to Popup public API 2025-11-14 09:30:44 +02:00
edgett
5ff30874dd Add CloseOnClickOutside parameter to Popup (#2355)
* Add CloseOnClickOutside parameter to Popup

Add a parameter called CloseOnClickOutside Popup, default is true. This maintains the current behavior. If set false, the Popup will remain open with the user click outside of it.

* Fix js function usage

Updated the parameters for the Radzen.openPopup JS function to include closeOnDocumentClick.
2025-11-14 09:27:36 +02:00
Vladimir Enchev
94550006c4 RadzenPivotDataGrid code improved 2025-11-13 15:54:11 +02:00
yordanov
202636ce72 Add BlackFriday offer 2025-11-13 13:12:12 +02:00
Vladimir Enchev
cae8c6f622 version updated 2025-11-13 10:37:42 +02:00
Vladimir Enchev
0e03c4377f PivotDataGrid set filter value should invalidate filter 2025-11-13 10:32:16 +02:00
Vladimir Enchev
7497ea1262 PivotDataGrid dynamic data support added 2025-11-13 10:19:37 +02:00
Atanas Korchev
d68bb34f6f Update getting started to include .NET 10. 2025-11-12 12:38:43 +02:00
Vladimir Enchev
732a6f4942 DataGrid sort ambiguous match found 2025-11-12 09:40:55 +02:00
Atanas Korchev
70fb896ae1 Update CI workflow to use .NET 10 2025-11-12 08:04:17 +02:00
Vladimir Enchev
034eae6722 Version updated 2025-11-11 17:50:22 +02:00
Vladimir Enchev
3472949bf0 sdk updated 2025-11-11 17:46:25 +02:00
Vladimir Enchev
c333b8ca30 Net10 support added (#2353)
* NET10 support added

* DataGrid Paging Disappears on Click in .NET Core 10.0.0 RC

Fix #2286

* various fixes

* KnownIPNetworks used instead KnownNetworks

* build fixed
2025-11-11 17:43:29 +02:00
Vladimir Enchev
17e3fbdabf version updated 2025-11-10 10:27:36 +02:00
Vladimir Enchev
30c5c9dfaf More strict GetDeclaredMethods() for FirstOrDefault(), LastOrDefault(), Cast() and Distinct()
Fix #2343
2025-11-10 09:41:06 +02:00
Vladimir Enchev
585d1ee38a DropDown ReadOnly XML description fixed 2025-11-10 09:37:02 +02:00
Vladimir Enchev
3255afb487 DialogService method CloseSideAsync made virtual
Fix #2349
2025-11-10 09:28:07 +02:00
Vladimir Enchev
9f75648b50 DropDownDataGrid exception with sorting when no columns are defined 2025-11-10 09:18:21 +02:00
yordanov
50406f8984 Update Material Icons font. Resolves #2342 2025-11-07 15:05:05 +02:00
Atanas Korchev
f24c7ebc5f Add demo showing how to use Google Fonts. 2025-11-07 13:34:18 +02:00
Vladimir Enchev
75dcecfab0 Version updated 2025-11-07 08:18:47 +02:00
Atanas Korchev
34c603ce53 Revert "Cleanup code a little (#2336)"
This reverts commit d816d841a8.
2025-11-07 07:01:16 +02:00
Vladimir Enchev
7785a73876 Version updated 2025-11-06 16:26:34 +02:00
Vladimir Enchev
cfa8f731f2 DropDown FooterTemplate added
Close #2344
2025-11-06 16:25:59 +02:00
Ondrej Bach
5ecd05c7b3 Additional parameters passed from RadzenDropDownDataGrid to RadzenDataGrid (#2347)
* Inject ServiceProvider to RadzenDataAnnotationValidator to use it within ValidationContext

* Fix ... add inject attribute

* Add pass-through parameters AllowColumnPicking, AllowColumnReorder and PageSizeOptions to DropDownDataGrid

---------

Co-authored-by: Ondrej Bach <ondrej.bach@external.drivalia.com>
2025-11-06 16:17:08 +02:00
Vladimir Enchev
4ec95c2f1c Various types extracted in separate files 2025-11-05 14:31:34 +02:00
Ondrej Bach
9e4413b02e Inject ServiceProvider to RadzenDataAnnotationValidator to use it wit… (#2339)
* Inject ServiceProvider to RadzenDataAnnotationValidator to use it within ValidationContext

* Fix ... add inject attribute

---------

Co-authored-by: Ondrej Bach <ondrej.bach@external.drivalia.com>
2025-11-04 18:43:12 +02:00
Vladimir Enchev
38d0d689b3 DataGrid CollectionFilterMode improved and added example 2025-11-04 11:38:06 +02:00
Vladimir Enchev
799c5e9e4e DataGrid column CollectionFilterMode property added
Close #2313
2025-11-04 10:27:11 +02:00
joriverm
b98dffda8f Add Immediate to Radzen Password (#2337)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-11-03 19:08:12 +02:00
joriverm
d816d841a8 Cleanup code a little (#2336)
* TextArea/Box change tests extended

fixup copy paste fail

* Cleanup warnings about FieldIdentifier.FieldName not able to be null

its defined as a non-nullable string, and an empty string is considerd valid

---------

Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-11-03 19:06:46 +02:00
788 changed files with 24990 additions and 11484 deletions

View File

@@ -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.

View File

@@ -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
View 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
View 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>

View File

@@ -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"]

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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]

View File

@@ -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")]

View File

@@ -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()
{

View File

@@ -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));
}
}
}

View File

@@ -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]

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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"));
}
}

View File

@@ -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]

View File

@@ -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]

View File

@@ -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);
}
}
}

View File

@@ -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 =>
{

View File

@@ -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; }

View File

@@ -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);
}
}
}

View File

@@ -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">

View 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}");
}
}
}

View File

@@ -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));

View 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);
}
}
}

View File

@@ -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

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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 &&

View File

@@ -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);
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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)
}
});
}

View File

@@ -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; }
}

View 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
View 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

View 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; }
}

View File

@@ -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
/// &lt;RadzenButton Text="Show context menu" ContextMenu=@(args => ShowContextMenuWithContent(args)) /&gt;
/// @code {
/// void ShowContextMenuWithContent(MouseEventArgs args) =&gt; ContextMenuService.Open(args, ds =&gt;
/// @&lt;RadzenMenu Click="OnMenuItemClick"&gt;
/// &lt;RadzenMenuItem Text="Item1" Value="1"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="Item2" Value="2"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="More items" Value="3"&gt;
/// &lt;RadzenMenuItem Text="More sub items" Value="4"&gt;
/// &lt;RadzenMenuItem Text="Item1" Value="5"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="Item2" Value="6"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenu&gt;);
///
/// 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
/// &lt;RadzenButton Text="Show context menu" ContextMenu=@(args => ShowContextMenuWithContent(args)) /&gt;
/// @code {
/// void ShowContextMenuWithContent(MouseEventArgs args) =&gt; ContextMenuService.Open(args, ds =&gt;
/// @&lt;RadzenMenu Click="OnMenuItemClick"&gt;
/// &lt;RadzenMenuItem Text="Item1" Value="1"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="Item2" Value="2"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="More items" Value="3"&gt;
/// &lt;RadzenMenuItem Text="More sub items" Value="4"&gt;
/// &lt;RadzenMenuItem Text="Item1" Value="5"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;RadzenMenuItem Text="Item2" Value="6"&gt;&lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenuItem&gt;
/// &lt;/RadzenMenu&gt;);
///
/// 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; }
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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>

View File

@@ -0,0 +1,18 @@
namespace Radzen;
/// <summary>
/// CoordinateSystem enum
/// </summary>
public enum CoordinateSystem
{
/// <summary>
/// Cartesian CoordinateSystem
/// </summary>
Cartesian,
/// <summary>
/// Polar CoordinateSystem
/// </summary>
Polar
}

View File

@@ -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>

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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
}

View 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
}

View 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
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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; }
}

View 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
}

View 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; }
}

View 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; }
}

View File

@@ -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
View 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
View 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

View 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>

View File

@@ -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&lt;T&gt;, 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();

View 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>();
}

View 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; }
}

View File

@@ -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)));

View File

@@ -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;
}
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
}
}
}

View 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
View 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);
}
}

View 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
}

View 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; }
}

View 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
}

View 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
View 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
}

View File

@@ -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()

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Radzen;
/// <summary>
/// Supplies information about a <see cref="Radzen.Blazor.RadzenTemplateForm{TItem}.InvalidSubmit" /> event that is being raised.
/// </summary>
public class FormInvalidSubmitEventArgs
{
/// <summary>
/// Gets the validation errors.
/// </summary>
public IEnumerable<string>? Errors { get; set; }
}

View File

@@ -0,0 +1,18 @@
namespace Radzen;
/// <summary>
/// Frozen Column Position enum
/// </summary>
public enum FrozenColumnPosition
{
/// <summary>
/// Freeze column to the left
/// </summary>
Left,
/// <summary>
/// Freeze column to the right
/// </summary>
Right
}

Some files were not shown because too many files have changed in this diff Show More