Files
radzen-blazor/Radzen.Blazor/RadzenPickList.razor.cs
2026-07-03 08:34:20 +03:00

695 lines
26 KiB
C#

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Radzen;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
/// <summary>
/// RadzenPickList component.
/// </summary>
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
public partial class RadzenPickList<TItem> : RadzenComponent
{
/// <summary>
/// Gets or sets the edit context.
/// </summary>
[CascadingParameter]
public EditContext? EditContext { get; set; }
/// <summary>
/// Gets or sets the source expression used to create the FieldIdentifier for source validation.
/// </summary>
[Parameter]
public Expression<Func<IEnumerable<TItem>?>>? SourceExpression { get; set; }
/// <summary>
/// Gets or sets the target expression used to create the FieldIdentifier for target validation.
/// </summary>
[Parameter]
public Expression<Func<IEnumerable<TItem>?>>? TargetExpression { get; set; }
FieldIdentifier? sourceFieldIdentifier;
FieldIdentifier? targetFieldIdentifier;
/// <summary>
/// Gets or sets a value indicating whether it is allowed to move all items.
/// </summary>
/// <value><c>true</c> if c allowed to move all items; otherwise, <c>false</c>.</value>
[Parameter]
public bool AllowMoveAll { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether it is allowed to move all items from source to target.
/// </summary>
/// <value><c>true</c> if c allowed to move all items from source to target; otherwise, <c>false</c>.</value>
[Parameter]
public bool AllowMoveAllSourceToTarget { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether it is allowed to move all items from target to source.
/// </summary>
/// <value><c>true</c> if c allowed to move all items from target to source; otherwise, <c>false</c>.</value>
[Parameter]
public bool AllowMoveAllTargetToSource { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to move all or only avaialable after filter items.
/// </summary>
/// <value><c>true</c> if c allowed to move filtered items only; otherwise, <c>false</c>.</value>
[Parameter]
public bool MoveFilteredItemsOnlyOnMoveAll { get; set; }
/// <summary>
/// Gets or sets a value indicating whether multiple selection is allowed.
/// </summary>
/// <value><c>true</c> if multiple selection is allowed; otherwise, <c>false</c>.</value>
[Parameter]
public bool Multiple { get; set; }
/// <summary>
/// Gets or sets a value indicating whether selecting all items is allowed.
/// </summary>
/// <value><c>true</c> if selecting all items is allowed; otherwise, <c>false</c>.</value>
[Parameter]
public bool AllowSelectAll { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether virtualization is enabled for the source and target listboxes.
/// </summary>
/// <value><c>true</c> if virtualization is enabled; otherwise, <c>false</c>.</value>
[Parameter]
public bool AllowVirtualization { get; set; }
/// <summary>
/// Gets or sets a value indicating whether component is disabled.
/// </summary>
/// <value><c>true</c> if component is disabled; otherwise, <c>false</c>.</value>
[Parameter]
public bool Disabled { get; set; }
/// <summary>
/// Gets or sets the source header
/// </summary>
/// <value>The source header.</value>
[Parameter]
public RenderFragment? SourceHeader { get; set; }
/// <summary>
/// Gets or sets the target header
/// </summary>
/// <value>The target header.</value>
[Parameter]
public RenderFragment? TargetHeader { get; set; }
/// <summary>
/// Gets or sets the common placeholder
/// </summary>
/// <value>The common placeholder.</value>
[Parameter]
public string? Placeholder { get; set; }
/// <summary>
/// Gets or sets the source placeholder
/// </summary>
/// <value>The source placeholder.</value>
[Parameter]
public string? SourcePlaceholder { get; set; }
/// <summary>
/// Gets or sets the target placeholder
/// </summary>
/// <value>The target placeholder.</value>
[Parameter]
public string? TargetPlaceholder { get; set; }
/// <summary>
/// Gets or sets the text property
/// </summary>
/// <value>The text property.</value>
[Parameter]
public string? TextProperty { get; set; }
/// <summary>
/// Gets or sets the value property
/// </summary>
/// <value>The value property.</value>
[Parameter]
public string? ValueProperty { get; set; }
/// <summary>
/// Gets or sets the disabled property
/// </summary>
/// <value>The disabled property.</value>
[Parameter]
public string? DisabledProperty { get; set; }
/// <summary>
/// Gets or sets the template.
/// </summary>
/// <value>The template.</value>
[Parameter]
public RenderFragment<TItem>? Template { get; set; }
/// <summary>
/// Gets or sets the source template. Overrides <see cref="Template"/>.
/// </summary>
/// <value>The source template.</value>
[Parameter]
public RenderFragment<TItem>? SourceTemplate { get; set; }
/// <summary>
/// Gets or sets the target template. Overrides <see cref="Template"/>.
/// </summary>
/// <value>The target template.</value>
[Parameter]
public RenderFragment<TItem>? TargetTemplate { get; set; }
/// <summary>
/// Gets or sets the select all text.
/// </summary>
/// <value>The select all text.</value>
[Parameter]
public string? SelectAllText { get; set; }
private string? emptyText;
/// <summary>
/// Gets or sets the empty text shown when a list has no items.
/// </summary>
/// <value>The empty text.</value>
[Parameter]
public string EmptyText { get => emptyText ?? Localize(nameof(RadzenStrings.PickList_EmptyText)); set => emptyText = value; }
private string? sourceAriaLabel;
/// <summary>
/// Gets or sets the aria-label of the source list. Ignored when <see cref="SourceHeader"/> is rendered - the source list is labelled by the header instead.
/// </summary>
/// <value>The source list aria-label.</value>
[Parameter]
public string SourceAriaLabel { get => sourceAriaLabel ?? Localize(nameof(RadzenStrings.PickList_SourceAriaLabel)); set => sourceAriaLabel = value; }
private string? targetAriaLabel;
/// <summary>
/// Gets or sets the aria-label of the target list. Ignored when <see cref="TargetHeader"/> is rendered - the target list is labelled by the header instead.
/// </summary>
/// <value>The target list aria-label.</value>
[Parameter]
public string TargetAriaLabel { get => targetAriaLabel ?? Localize(nameof(RadzenStrings.PickList_TargetAriaLabel)); set => targetAriaLabel = value; }
/// <summary>
/// Gets or sets the empty text shown when the source list has no items. Overrides <see cref="EmptyText"/>.
/// </summary>
/// <value>The source empty text.</value>
[Parameter]
public string? SourceEmptyText { get; set; }
/// <summary>
/// Gets or sets the empty text shown when the target list has no items. Overrides <see cref="EmptyText"/>.
/// </summary>
/// <value>The target empty text.</value>
[Parameter]
public string? TargetEmptyText { get; set; }
/// <summary>
/// Gets or sets the empty template shown when a list has no items.
/// </summary>
/// <value>The empty template.</value>
[Parameter]
public RenderFragment? EmptyTemplate { get; set; }
/// <summary>
/// Gets or sets the empty template shown when the source list has no items. Overrides <see cref="EmptyTemplate"/>.
/// </summary>
/// <value>The source empty template.</value>
[Parameter]
public RenderFragment? SourceEmptyTemplate { get; set; }
/// <summary>
/// Gets or sets the empty template shown when the target list has no items. Overrides <see cref="EmptyTemplate"/>.
/// </summary>
/// <value>The target empty template.</value>
[Parameter]
public RenderFragment? TargetEmptyTemplate { get; set; }
/// <summary>
/// Gets or sets the row render callback. Use it to set row attributes.
/// </summary>
/// <value>The row render callback.</value>
[Parameter]
public Action<PickListItemRenderEventArgs<TItem>>? ItemRender { get; set; }
void OnSourceItemRender(ListBoxItemRenderEventArgs<object?> args)
{
if (ItemRender != null)
{
var newArgs = new PickListItemRenderEventArgs<TItem>();
newArgs.Item = args.Item != null ? (TItem)args.Item : default(TItem);
ItemRender(newArgs);
newArgs.Attributes.ToList().ForEach(k => args.Attributes.Add(k.Key, k.Value));
args.Visible = newArgs.Visible;
args.Disabled = newArgs.Disabled;
}
}
void OnTargetItemRender(ListBoxItemRenderEventArgs<object?> args)
{
if (ItemRender != null)
{
var newArgs = new PickListItemRenderEventArgs<TItem>();
newArgs.Item = args.Item != null ? (TItem)args.Item : default(TItem);
ItemRender(newArgs);
newArgs.Attributes.ToList().ForEach(k => args.Attributes.Add(k.Key, k.Value));
args.Visible = newArgs.Visible;
args.Disabled = newArgs.Disabled;
}
}
private RenderFragment<dynamic>? SourceListBoxTemplate
{
get
{
if (SourceTemplate != null)
{
return item => SourceTemplate(item);
}
if (Template != null)
{
return item => Template(item);
}
return null;
}
}
private RenderFragment<dynamic>? TargetListBoxTemplate
{
get
{
if (TargetTemplate != null)
{
return item => TargetTemplate(item);
}
if (Template != null)
{
return item => Template(item);
}
return null;
}
}
/// <summary>
/// Gets or sets value if filtering is allowed.
/// </summary>
/// <value>The allow filtering value.</value>
[Parameter]
public bool AllowFiltering { get; set; }
/// <summary>
/// Gets or sets value if headers are shown.
/// </summary>
/// <value>If headers are shown value.</value>
[Parameter]
public bool ShowHeader { get; set; } = true;
/// <summary>
/// Gets or sets the buttons spacing
/// </summary>
/// <value>The buttons spacing.</value>
[Parameter]
public string? ButtonGap { get; set; }
/// <summary>
/// Gets or sets the orientation
/// </summary>
/// <value>The orientation.</value>
[Parameter]
public Orientation Orientation { get; set; } = Orientation.Horizontal;
/// <summary>
/// Gets or sets the buttons style
/// </summary>
/// <value>The buttons style.</value>
[Parameter]
public JustifyContent ButtonJustifyContent { get; set; } = JustifyContent.Center;
/// <summary>
/// Gets or sets the buttons style
/// </summary>
/// <value>The buttons style.</value>
[Parameter]
public ButtonStyle ButtonStyle { get; set; } = ButtonStyle.Primary;
/// <summary>
/// Gets or sets the design variant of the buttons.
/// </summary>
/// <value>The variant of the buttons.</value>
[Parameter]
public Variant ButtonVariant { get; set; } = Variant.Text;
/// <summary>
/// Gets or sets the color shade of the buttons.
/// </summary>
/// <value>The color shade of the buttons.</value>
[Parameter]
public Shade ButtonShade { get; set; } = Shade.Default;
/// <summary>
/// Gets or sets the buttons size.
/// </summary>
/// <value>The buttons size.</value>
[Parameter]
public ButtonSize ButtonSize { get; set; } = ButtonSize.Medium;
private string? sourceToTargetTitle;
/// <summary>
/// Gets or sets the source to target title
/// </summary>
/// <value>The source to target title.</value>
[Parameter]
public string SourceToTargetTitle { get => sourceToTargetTitle ?? Localize(nameof(RadzenStrings.PickList_SourceToTargetTitle)); set => sourceToTargetTitle = value; }
private string? selectedSourceToTargetTitle;
/// <summary>
/// Gets or sets the selected source to target title
/// </summary>
/// <value>The selected source to target title.</value>
[Parameter]
public string SelectedSourceToTargetTitle { get => selectedSourceToTargetTitle ?? Localize(nameof(RadzenStrings.PickList_SelectedSourceToTargetTitle)); set => selectedSourceToTargetTitle = value; }
private string? targetToSourceTitle;
/// <summary>
/// Gets or sets the target to source title
/// </summary>
/// <value>The target to source title.</value>
[Parameter]
public string TargetToSourceTitle { get => targetToSourceTitle ?? Localize(nameof(RadzenStrings.PickList_TargetToSourceTitle)); set => targetToSourceTitle = value; }
private string? selectedTargetToSourceTitle;
/// <summary>
/// Gets or sets the selected target to source title
/// </summary>
/// <value>The selected target to source title.</value>
[Parameter]
public string SelectedTargetToSourceTitle { get => selectedTargetToSourceTitle ?? Localize(nameof(RadzenStrings.PickList_SelectedTargetToSourceTitle)); set => selectedTargetToSourceTitle = value; }
/// <summary>
/// Gets or sets the source to target icon
/// </summary>
/// <value>The source to target icon.</value>
[Parameter]
public string SourceToTargetIcon { get; set; } = "keyboard_double_arrow_right";
/// <summary>
/// Gets or sets the selected source to target icon
/// </summary>
/// <value>The selected source to target icon.</value>
[Parameter]
public string SelectedSourceToTargetIcon { get; set; } = "keyboard_arrow_right";
/// <summary>
/// Gets or sets the target to source icon
/// </summary>
/// <value>The target to source icon.</value>
[Parameter]
public string TargetToSourceIcon { get; set; } = "keyboard_double_arrow_left";
/// <summary>
/// Gets or sets the selected target to source icon
/// </summary>
/// <value>The selected target to source icon.</value>
[Parameter]
public string SelectedTargetToSourceIcon { get; set; } = "keyboard_arrow_left";
/// <summary>
/// Gets the final CSS style rendered by the component. Combines it with a <c>style</c> custom attribute.
/// </summary>
protected string? GetStyle()
{
if (Attributes != null && Attributes.TryGetValue("style", out var style) && !string.IsNullOrEmpty(Convert.ToString(@style, CultureInfo.InvariantCulture)))
{
return $"{Style} {@style}";
}
return Style;
}
/// <inheritdoc />
protected override string GetComponentCssClass()
{
return "rz-picklist";
}
/// <summary>
/// Gets or sets the source collection.
/// </summary>
/// <value>The source collection.</value>
[Parameter]
public IEnumerable<TItem>? Source { get; set; }
IEnumerable<TItem>? source;
/// <summary>
/// Gets or sets the source changed.
/// </summary>
/// <value>The source changed.</value>
[Parameter]
public EventCallback<IEnumerable<TItem>> SourceChanged { get; set; }
/// <summary>
/// Gets or sets the target collection.
/// </summary>
/// <value>The target collection.</value>
[Parameter]
public IEnumerable<TItem>? Target { get; set; }
IEnumerable<TItem>? target;
/// <summary>
/// Gets or sets the target changed.
/// </summary>
/// <value>The target changed.</value>
[Parameter]
public EventCallback<IEnumerable<TItem>> TargetChanged { get; set; }
/// <summary>
/// Gets or sets the callback that is invoked when the selected source items change.
/// </summary>
/// <value>The selected source items changed callback.</value>
[Parameter]
public EventCallback<IEnumerable<TItem>> SelectedSourceChanged { get; set; }
/// <summary>
/// Gets or sets the callback that is invoked when the selected target items change.
/// </summary>
/// <value>The selected target items changed callback.</value>
[Parameter]
public EventCallback<IEnumerable<TItem>> SelectedTargetChanged { get; set; }
/// <summary>
/// Gets or sets the callback that is invoked when items are moved between the source and target collections.
/// Fires after <see cref="SourceChanged"/> and <see cref="TargetChanged"/>, so the bound collections already reflect the move.
/// </summary>
/// <value>The move callback.</value>
[Parameter]
public EventCallback<PickListMoveEventArgs<TItem>> Move { get; set; }
object? selectedSourceItems;
object? selectedTargetItems;
async Task OnSelectedSourceItemsChanged(object value)
{
selectedSourceItems = value;
await SelectedSourceChanged.InvokeAsync(GetSelectedSources());
}
async Task OnSelectedTargetItemsChanged(object value)
{
selectedTargetItems = value;
await SelectedTargetChanged.InvokeAsync(GetSelectedTargets());
}
string? sourceSearchText;
string? targetSearchText;
/// <summary>
/// Set parameters as an asynchronous operation.
/// </summary>
/// <param name="parameters">The parameters.</param>
/// <returns>A Task representing the asynchronous operation.</returns>
public override async Task SetParametersAsync(ParameterView parameters)
{
var sourceChanged = parameters.DidParameterChange(nameof(Source), Source);
if (sourceChanged)
{
source = parameters.GetValueOrDefault<IEnumerable<TItem>>(nameof(Source));
}
var targetChanged = parameters.DidParameterChange(nameof(Target), Target);
if (targetChanged)
{
target = parameters.GetValueOrDefault<IEnumerable<TItem>>(nameof(Target));
}
if (parameters.DidParameterChange(nameof(Multiple), Multiple))
{
selectedSourceItems = null;
selectedTargetItems = null;
}
await base.SetParametersAsync(parameters);
if (EditContext != null)
{
if (SourceExpression != null && sourceFieldIdentifier == null)
{
sourceFieldIdentifier = FieldIdentifier.Create(SourceExpression);
}
if (TargetExpression != null && targetFieldIdentifier == null)
{
targetFieldIdentifier = FieldIdentifier.Create(TargetExpression);
}
}
}
/// <summary>
/// Returns a collection of TItem that are selected in the source list.
/// </summary>
/// <returns></returns>
public IEnumerable<TItem> GetSelectedSources() => GetSelectedItems(selectedSourceItems, source);
/// <summary>
/// Returns a collection of TItem that are selected in the target list.
/// </summary>
/// <returns></returns>
public IEnumerable<TItem> GetSelectedTargets() => GetSelectedItems(selectedTargetItems, target);
IEnumerable<TItem> GetSelectedItems(object? selectedItems, IEnumerable<TItem>? data)
{
if (selectedItems == null)
{
return Enumerable.Empty<TItem>();
}
var selected = Multiple
? (selectedItems as IEnumerable)?.Cast<object?>().ToList() ?? new List<object?>()
: new List<object?> { selectedItems };
if (selected.Count > 0 && selected.All(o => o is TItem))
{
return selected.Cast<TItem>();
}
if (!string.IsNullOrEmpty(ValueProperty) && data != null)
{
var selectedValues = new HashSet<object?>(selected);
return data.Where(item => selectedValues.Contains(PropertyAccess.GetValue(item, ValueProperty)));
}
return Enumerable.Empty<TItem>();
}
RadzenListBox<object>? sourceListBox;
RadzenListBox<object>? targetListBox;
private async Task Update(bool sourceToTarget, IEnumerable<TItem>? items)
{
IEnumerable<TItem> movedItems;
if (sourceToTarget)
{
if (items != null)
{
movedItems = items as IList<TItem> ?? items.ToList();
target = (target ?? Enumerable.Empty<TItem>()).Concat(movedItems);
source = (source ?? Enumerable.Empty<TItem>()).Except(movedItems);
}
else
{
movedItems = (MoveFilteredItemsOnlyOnMoveAll ? sourceListBox?.GetView()?.Cast<TItem>() ?? Enumerable.Empty<TItem>() : source ?? Enumerable.Empty<TItem>()).ToList();
target = (target ?? Enumerable.Empty<TItem>()).Concat(movedItems);
source = MoveFilteredItemsOnlyOnMoveAll && source != null ? source.Except(movedItems) : null;
}
}
else
{
if (items != null)
{
movedItems = items as IList<TItem> ?? items.ToList();
source = (source ?? Enumerable.Empty<TItem>()).Concat(movedItems);
target = (target ?? Enumerable.Empty<TItem>()).Except(movedItems);
}
else
{
movedItems = (MoveFilteredItemsOnlyOnMoveAll ? targetListBox?.GetView()?.Cast<TItem>() ?? Enumerable.Empty<TItem>() : target ?? Enumerable.Empty<TItem>()).ToList();
source = (source ?? Enumerable.Empty<TItem>()).Concat(movedItems);
target = MoveFilteredItemsOnlyOnMoveAll && target != null ? target.Except(movedItems) : null;
}
}
source = source?.Any() == true ? source : null;
target = target?.Any() == true ? target : null;
await SourceChanged.InvokeAsync(source);
await TargetChanged.InvokeAsync(target);
if (Move.HasDelegate)
{
await Move.InvokeAsync(new PickListMoveEventArgs<TItem>
{
MoveDirectionToTarget = sourceToTarget,
Items = movedItems,
});
}
if (EditContext != null)
{
if (sourceFieldIdentifier.HasValue)
{
EditContext.NotifyFieldChanged(sourceFieldIdentifier.Value);
}
if (targetFieldIdentifier.HasValue)
{
EditContext.NotifyFieldChanged(targetFieldIdentifier.Value);
}
}
selectedSourceItems = null;
selectedTargetItems = null;
if (items == null && !MoveFilteredItemsOnlyOnMoveAll)
{
sourceSearchText = null;
targetSearchText = null;
}
StateHasChanged();
}
private async Task SourceToTarget() => await Update(true, null);
private async Task SelectedSourceToTarget() => await Update(true, GetSelectedSources());
private async Task TargetToSource() => await Update(false, null);
private async Task SelectedTargetToSource() => await Update(false, GetSelectedTargets());
}
}