Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New SelectIndexedList Component #3317

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

lolochristen
Copy link

The components Select and SelectList are not able to directly return and bind to an item from the assigned list. I created a new component with this capability as the existing SelectList would have most likely experienced a breaking change. the new component uses the index of a IList for the SelectItem and synchronizes with SelectedItem property.
Please consider this capability as I at was a crucial missing feature in my projects. Feel free to integrate directly or replace SelectList.

Example:

<SelectIndexedList TItem="Item" Data="items" @bind-SelectedItem="@selectedItem" TextField="@((item)=>item.Name)"  /> 
@code {

    private IList<Item> items = new List<Item>()
    {
        new Item() { Id = 1, Name = "Value 1" },
        new Item() { Id = 2, Name = "Value 2" }, 
        new Item() { Id = 3, Name = "Value 3" }
    };

    private Item? selectedItem;

    internal class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

@stsrki
Copy link
Collaborator

stsrki commented Jan 13, 2022

Seems good, although I would like it better if it was part of an existing component. I will try to make work on SelectList in the next few days, my schedule is full at the moment so I cannot properly test it. Thanks for the PR, btw 💪

@stsrki
Copy link
Collaborator

stsrki commented Jan 14, 2022

The problem with SelectIndexedList is that it would lead to several problems.

  1. Data-Annotation validation would not work because it doesn't bind to any field on an TItem
  2. @key parameter would potentially be invalid in certain cases if Data is changed
  3. Use of .Wait(); in synchronous context

This is my first try at adding the SelectedIndex to the SelectList. Seems to be working fine but more tests are welcome on your side.

Test

<Button Color="Color.Primary" Clicked="@(()=>{ selectedItem = items[1]; return Task.CompletedTask; })">SELECT ITEM</Button>
<Button Color="Color.Danger" Clicked="@(()=>{ items.RemoveAt(1); return Task.CompletedTask; })">REMOVE ONE</Button>

<SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem" ValueField="@((item)=>item.Id)" TextField="@((item)=>item.Name)" />

<Div>
    Selected item: @selectedItem?.Name
</Div>
@code {
    private IList<Item> items = new List<Item>()
    {
        new Item() { Id = 1, Name = "Value 1" },
        new Item() { Id = 2, Name = "Value 2" },
        new Item() { Id = 3, Name = "Value 3" },
        new Item() { Id = 4, Name = "Value 4" },
        new Item() { Id = 5, Name = "Value 5" },
    };

    private Item? selectedItem;

    internal class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

if ( !selectedValue.Equals( value ) )
{
selectedValue = value;
SelectedValueChanged.InvokeAsync( selectedValue ).ConfigureAwait( false );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must not call InvokeAsync in synchronous code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at asp.net core components and the pattern they use is to trigger changed events use within a internal CurrentValueProperty. I propose to the same as Property can be changed from different sources. And there is no other way then raising InvokeAsync in synch code. At least I use no _ to indicate that code does not care of the task output.

#endregion

#region Methods

protected Task HandleSelectedValueChanged( TValue value )
{
SelectedValue = value;
return SelectedValueChanged.InvokeAsync( value );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this is removed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As changes also can be trigger from SelectedItem change, the raising of the events shall when the property changes.

/// Returns true if SelectedItem property is supported.
/// </summary>
public bool SelectedItemSupported =>
ValueField == null && Data is IList && typeof(TValue) == typeof(int);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why support only typeof(int)? I think practically any TValue type should be indexed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a IList, the index is always int. A standard collection supporting other types as index would be a dictionary which is not assignable from IEnumerable. We would need to move to object for Data (btw as WPF/UWP does with ItemSource)

@lolochristen
Copy link
Author

Wow, fast response! First, I agree with some of your comments that's why I tried to merge the features into SelectList. To avoid breaking changes, the solution is not pretty but at least we have a bindable SelectedItem property and Validation works as well changes of the Data collection. I have tested it successfully with the following scenarios:

<SelectList TItem="Item" TValue="int" Data="itemsList" @bind-SelectedItem="@selectedItem" TextField="@((item) => item.Name)">

@*selectedItem = Item,  selectedValue = index and default Item *@
<SelectList TItem="Item" TValue="int" Data="itemsList" @bind-SelectedItem="@selectedItem3" TextField="@((item)=>item.Name)" DefaultItemText="make your choice" DefaultItemValue="-1" /> 

@*selectedValue = index *@
<SelectList TItem="Item" TValue="int" Data="itemsList" @bind-SelectedValue="@selectedValue" TextField="@((item)=>item.Name)"  />  

@* previous behavior with ValueField *@
<SelectList TItem="Item" TValue="int" Data="itemsList" @bind-SelectedValue="@selectedValue2" TextField="@((item)=>item.Name)" ValueField="@((item)=>item.Id)"  /> 

@stsrki
Copy link
Collaborator

stsrki commented Feb 10, 2022

@David-Moreira can you try looking at this if you have time?

@David-Moreira David-Moreira self-assigned this Feb 10, 2022
@David-Moreira
Copy link
Contributor

David-Moreira commented Feb 14, 2022

I'll take a look over these days.
To start off. Could you guys clarify what does this component solve?
I kinda don't get this phrase:

"The components Select and SelectList are not able to directly return and bind to an item from the assigned list. "

Does SelectList not already support this?
The shown example also seems to be supported by SelectList... but I'm sure I am missing something elementar.... again could you guys enlighten me, please :D

Edit:
Don't bother too much explaining I'll just test it myself later, I'm sure I'll figure it out. haha, I'll let you know otherwise.

@stsrki
Copy link
Collaborator

stsrki commented Feb 14, 2022

SelectList works with values and texts, same as DropdownLis,t for example. With this PR we could bind to an actual element instead of just the value.

@David-Moreira
Copy link
Contributor

Ah great! Yea no point in doing a new component. We should also update the other one's if they do not support it.

@David-Moreira
Copy link
Contributor

Hello @lolochristen ,
So I was starting to do some testing based on your examples, can you take a look at these:

1- If I mutate selectedItem SelectList is not updating the dropdown value, it remains value1.
image
Test code:

<Paragraph>
    <Button Color="Color.Primary" Clicked="@(()=>{ selectedItem = items[1]; return InvokeAsync(StateHasChanged); })">SELECT ITEM</Button>
    <Button Color="Color.Danger" Clicked="@(()=>{ items.RemoveAt(1); return Task.CompletedTask; })">REMOVE ONE</Button>

    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem" ValueField="@((item)=>item.Id)" TextField="@((item)=>item.Name)" />
    <Paragraph>Selected item: @selectedItem?.Name</Paragraph>
</Paragraph>

2- If I have two SelectList and I try to mutate selectedItem it will throw this error:
image

<Paragraph>
    <Button Color="Color.Primary" Clicked="@(()=>{ selectedItem = items[1]; return InvokeAsync(StateHasChanged); })">SELECT ITEM</Button>
    <Button Color="Color.Danger" Clicked="@(()=>{ items.RemoveAt(1); return Task.CompletedTask; })">REMOVE ONE</Button>

    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem" ValueField="@((item)=>item.Id)" TextField="@((item)=>item.Name)" />
    <Paragraph>Selected item: @selectedItem?.Name</Paragraph>
</Paragraph>

<Paragraph>
    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem2" TextField="@((item) => item.Name)" />
    <Paragraph>Selected item 2: @selectedItem2?.Name</Paragraph>
</Paragraph>

Finally here's the full code that I was going to start testing that's based on your examples, before I found these issues:

<Paragraph>
    <Button Color="Color.Primary" Clicked="@(()=>{ selectedItem = items[1]; return InvokeAsync(StateHasChanged); })">SELECT ITEM</Button>
    <Button Color="Color.Danger" Clicked="@(()=>{ items.RemoveAt(1); return Task.CompletedTask; })">REMOVE ONE</Button>

    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem" ValueField="@((item)=>item.Id)" TextField="@((item)=>item.Name)" />
    <Paragraph>Selected item: @selectedItem?.Name</Paragraph>
</Paragraph>

<Paragraph>
    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem2" TextField="@((item) => item.Name)" />
    <Paragraph>Selected item 2: @selectedItem2?.Name</Paragraph>
</Paragraph>



<Paragraph>
    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedItem="@selectedItem3" TextField="@((item)=>item.Name)" DefaultItemText="make your choice" DefaultItemValue="-1" />
    <Paragraph>Selected item 3: @selectedItem3?.Name</Paragraph>
</Paragraph>


<Paragraph>

    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedValue="@selectedValue" TextField="@((item)=>item.Name)" />
    <Paragraph>Selected value: @selectedValue</Paragraph>

</Paragraph>


<Paragraph>
    <SelectList TItem="Item" TValue="int" Data="items" @bind-SelectedValue="@selectedValue2" TextField="@((item)=>item.Name)" ValueField="@((item)=>item.Id)" />
    <Paragraph>Selected value 2: @selectedValue2</Paragraph>
</Paragraph>

@code {
    private IList<Item> items = new List<Item>()
    {
        new Item() { Id = 1, Name = "Value 1" },
        new Item() { Id = 2, Name = "Value 2" },
        new Item() { Id = 3, Name = "Value 3" },
        new Item() { Id = 4, Name = "Value 4" },
        new Item() { Id = 5, Name = "Value 5" },
    };

    private Item? selectedItem;
    private Item? selectedItem2;
    private Item? selectedItem3;

    private int selectedValue;
    private int selectedValue2;

    internal class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

@stsrki
Copy link
Collaborator

stsrki commented Feb 15, 2022

I would say the reason for the error is that GetKey should return an actual item and set that as a @key instead of the hash code.

@David-Moreira
Copy link
Contributor

@lolochristen just a friendly reminder. :) Understand if you're busy, of course.

@David-Moreira
Copy link
Contributor

@stsrki Let's resume this for 1.3?

Also, some time has passed, I'm having a hard time remembering how this component is different then a regular SelectList?

@stsrki
Copy link
Collaborator

stsrki commented May 16, 2023

@stsrki Let's resume this for 1.3?

Also, some time has passed, I'm having a hard time remembering how this component is different then a regular SelectList?

As far as I remember, this PR introduces an internal List that holds the Items, and then we can access them by the Index. Then again, I was thinking we should probably go forward with making something similar on the regular Select component. Have an internal List or Dictionary to hold data items. That way we could, in theory, solve problems with storing object types as values.

@David-Moreira
Copy link
Contributor

@stsrki Let's resume this for 1.3?
Also, some time has passed, I'm having a hard time remembering how this component is different then a regular SelectList?

As far as I remember, this PR introduces an internal List that holds the Items, and then we can access them by the Index. Then again, I was thinking we should probably go forward with making something similar on the regular Select component. Have an internal List or Dictionary to hold data items. That way we could, in theory, solve problems with storing object types as values.

I see. Let's investigate and we might abandon this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants