-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
For Avalonia.Controls.ListBox for Items Property, Support for IList<T> without non-generic IList #8764
Comments
I have added an example reproduction, with one INPC view-model, one collection with IList that works, and one IReadOnlyCollection that does not function. This example app asynchronously populates a list, and then removes items from the list updating the UI as it goes. It then slowly clears the IList and then tries to reattempt with the IReadOnlyList - which fails with the stack-trace appearing on-screen instead. |
INotifyCollectionChanged doesn't make sense together with IReadOnlyList semantically. |
How can there be a |
Yes, but it also fails with IList<T>. It only works with IList, which is a different and incompatible interface. My target interface in mind was IReadOnlyList<T> because in my mind a ListBox is unlike a DataGrid in that there's not the expectation of the control having inbuilt functionality to delete items from the collection. IList<T> extends IReadOnlyList<T> - so if we can get ListBox to work for IReadOnlyList<T> then it should also work for IList<T> |
I had a look in M$ - source code, how they implemented their This is just for information. If Avalonia finds a way to ship around that issue, even better. I'm just suprised they designed stuff like that. Happy coding |
I did have some early success in modifying the way the underlying provider indexes lists, by adding a non-IList interface, and then having multiple classes that implement that non-IList interface - one that takes in an IList, and another that takes in an IReadOnlyList<object> - which works because all IReadOnlyList<T> are castable to IReadOnlyList<object> because of the rules of Covariance. I may tidy that up later, and perhaps issue a PR as a fix, but @maxkatz6
Are you certain that you do not want ListBox to work with List types that do not implement IList (non-generic list) specifically? IList<T> does not extend IList - so you can have lists that are read-write but do not implement IList and the current implementation will never work with those kinds of lists if they also implement INotifyCollectionChanged. |
Not totally true, if |
That is a really good pickup. I can confirm that it does not function for T is struct List<String> stringList = new List<string>(); // start declaration
//List<object> objList = (List<object>)stringList; // does not compile
//IList<object> iObjList = (IList<object>)stringList; // invalid cast exception at runtime
IReadOnlyList<object> iReadOnlyObjList = (IReadOnlyList<Object>) stringList; // valid at both compile and run
List<Guid> guidList = new List<Guid>();// another declaration
//List<object> objList = (List<object>)guidList; // does not compile
//IReadOnlyList<object> objList = (IReadOnlyList<object>)guidList; // Invalid Cast Exception at runtime
//IReadOnlyList<ValueType> valueList = (IReadOnlyList<ValueType>)guidList; // Invalid Cast Exception at runtime
Span<Guid> spanValue = CollectionsMarshal.AsSpan(guidList); // kind of useless because it requires that we know what T is, and requires something that inherits from List<T> Which absolutely sinks my idea of how to solve this. Diving further into this, I'd like to thank Windows10CE#8553 on Discord for sharing their LabSharp experiments. With Code: using SharpLab.Runtime;
[JitGeneric(typeof(object))]
[JitGeneric(typeof(string))]
[JitGeneric(typeof(System.ValueType))]
[JitGeneric(typeof(System.Guid))]
public class MyFakeList<T> : MyFakeInterface<T>
{
public T Get()
{
return default;
}
}
public interface MyFakeInterface<out T>
{
T Get();
} Has Assembly
And explains further
I can't think of a great solution for making this work with structs as well as classes. If no-one can think of any solutions to this, I'll close the issue accepting 'By Design' and this closed issue can remain to serve as documentation that helps explain this behaviour and the IList (non-generic) requirement. |
This task can be achieved by creating wrapper classes that looks a bit like this: class ReadOnlyListWrapper<T> : IList, INotifyCollectionChanged
{
public ReadOnlyListWrapper(IReadOnlyList<T> source)
{
// store source as a field
}
// IList and INotifyCollectionChanged implementations that pass through to source
} There would be a second version for Either users can create such an object themselves, or Avalonia can use reflection to create one for its internal use when appropriate. |
I was thinking of something like that - but for IList<T> without IList is problematic where T : struct. I don't have a performant solution to making that implementation, and this is starting to become larger in scope than I had originally thought it could be when investigating this originally. |
What exactly is problematic? There is no casting of generic types going on here. Do you mean performance penalty of boxing of |
I meant the reflection-y check for the generic of IList<T> and/or IReadOnlyList<T> Turns out I was mistaken! IList<T> does NOT extend IReadOnlyList<T> ! But even so it's very possible for a collection to be supporting both IList<T> and INotifyCollectionChanged without implementing IList (non-generic) In those circumstances, the ListBox will fail to bind and throw an unhandled exception. Leveraging @tomenscape 's idea, I have the following, which works, but I dislike its use of reflection to construct the required object(s). private IList ProbeIEnumerableForAppropriateIndexer(IEnumerable source)
{
if (source is IList<object> castSource)
{
return new GenericListWrapper<object>(castSource);
}
if (source is IReadOnlyList<object> castSourceReadOnly)
{
return new ReadOnlyListWrapper<object>(castSourceReadOnly);
}
// It is not an IReadOnlyList<object> - but it could be an IReadOnlyList<T> where T : struct
// Reflection use here, will be slower, but the ideal scenarios have already been covered
// This is for an edge-case, if they don't want this to be slow, they should implement IList, or IReadOnlyList of an object
// Is there a way to warn in Avalonia Diagnostics of a slower compat-code-path ?
var type = source.GetType();
var interfaces = type.GetInterfaces();
for (int i = 0; i < interfaces.Length; i++)
{
var @interface = interfaces[i];
if (@interface is null)
continue;
// null check satisifed
if (@interface.GenericTypeArguments.Length == 1 && @interface.GetGenericTypeDefinition() == typeof(IList<>))
{
// found it. It is in fact an IList<T> where T : struct
try
{
var listWrapperType = typeof(GenericListWrapper<>);
var instance = Activator.CreateInstance(listWrapperType.MakeGenericType(@interface.GenericTypeArguments.Single()), source);
if (instance is not null)
return (IList)instance;
}
catch (Exception ex)
{
// TODO: Log cast failure here
}
}
if (@interface.GenericTypeArguments.Length == 1 && @interface.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))
{
// found it. It is in fact an IReadOnlyList<T> where T : struct
try
{
var readOnlyListWrapperType = typeof(ReadOnlyListWrapper<>);
var instance = Activator.CreateInstance(readOnlyListWrapperType.MakeGenericType(@interface.GenericTypeArguments.Single()), source);
if(instance is not null)
return (IList)instance;
}
catch(Exception ex)
{
// TODO: Log cast failure here
}
}
}
// alternative code:
// if (gs.GetType().GetInterfaces().Any(x => x.GenericTypeArguments.Length == 1 && x.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))) { /* ... */ }
// could check other specialized collection types here too
return new List<object>(source.Cast<object>());
} This code needs cleaning up, but it does the job. What are your opinions on this? |
My thoughts:
|
Avalonia currently tries to reduce reflection as it has performance and trimming issues. I don't know if that rare case is worth that much effort. Just my 2 cents. |
That line is identical to Line 53 - but perhaps you're right and it should be Line 52 instead - creating a new List which implements IList and is just a container for the enumerable. Agreed, we don't want to copy the items of the IEnumerable, Line 52 is preferred to Line 53. Gah, you're right about the I thought since we're testing for multiple interfaces here that the loop would be preferred to the GetInterface by string name form for performance reasons, but maybe I'm prematurely optimizing again.
Right, I agree with that, but if we're hitting that code section then we've already exhausted all of the high performing options - no IList (non-generic) and does not implement |
We can't accept code that relies on reflection in the main code base |
More nails in the coffin of this approach. Everything past the |
This leaves the following options, IMO:
Separately, both of the lines in ItemsSourceView which copy the source collection are dodgy. This is a view of a mutable object, not a copy! |
What could be changed regarding L52 and L53 to get an IList (non-generic) implementation out of an IEnumerable (non-generic) ? Is it inappropriate to create a new More stuff to think about, I may try out some things later tonight when I get home from work. |
I don't think |
A user may want to display the contents of a simple enumerable and then refresh the UI manually when they know/judge that it has changed. But since a copy was taken, this won't work. The case of an infinite enumerable is a pain, though you could throw an exception after enumerating |
So in future we could have different `ItemSourceView` concrete implementations that e.g. have an inner `IReadOnlyList<T>` in order to address #8764.
Make `SelectionModel.SelectedItems` and `SelectionModel.SelectedIndexes` implement `INotifyCollectionChanged` so that they can be bound to. As well as implementing `INotifyCollectionChanged` on the collections, we also had to implement `IList` (see #8764) so refactored this out into a base class. For the sake of simplicity, these collections only raise `Reset` for any change: this is may need to be changed later but I'd rather follow the KISS principle for the moment until something more complex is proven necessary. Fixes #15497
Make `SelectionModel.SelectedItems` and `SelectionModel.SelectedIndexes` implement `INotifyCollectionChanged` so that they can be bound to. As well as implementing `INotifyCollectionChanged` on the collections, we also had to implement `IList` (see #8764) so refactored this out into a base class. For the sake of simplicity, these collections only raise `Reset` for any change: this is may need to be changed later but I'd rather follow the KISS principle for the moment until something more complex is proven necessary. Fixes #15497
Make `SelectionModel.SelectedItems` and `SelectionModel.SelectedIndexes` implement `INotifyCollectionChanged` so that they can be bound to. As well as implementing `INotifyCollectionChanged` on the collections, we also had to implement `IList` (see #8764) so refactored this out into a base class. For the sake of simplicity, these collections only raise `Reset` for any change: this is may need to be changed later but I'd rather follow the KISS principle for the moment until something more complex is proven necessary. Fixes #15497
After running into this issue today I'd like to express my support for the third option. Currently the behavior when using INCC + View code behind: ViewModel.WhenAnyValue(vm => vm.ResultsBuffer)
.BindTo(this, v => v.ListBox.ItemsSource)
.DisposeWith(d); If If I change my code to this: ViewModel.WhenAnyValue(vm => vm.ResultsBuffer)
.Subscribe(buffer => ListBox.ItemsSource = buffer)
.DisposeWith(d); I get an exception that tells me that my collection needs to implement |
Describe the bug
For a DataContext View-Model that has Specialized Collections, collections may implement the IList<T> or IReadOnlyList<T> interfaces, neither of which implements non-generic IList interface.
Any such collections when they also implement INotifyCollectionChanged cause assignment of the DataContext involved throw an exception stating, correctly, that the collection in question does not implement IList, which is correct - it does not.
Thrown here
My expectations were that ListBoxes would work with generic collections, not only collections that implement IList
Related PR
To Reproduce
Steps to reproduce the behavior:
1.) Create a ViewModel that implements INPC, create a collection that implements IList<T> or IReadOnlyList<T> and INotifyCollectionChanged but does not implement IList
2.) Assign the collection as the DataContext of a Window or view such that you have a ListBox that has its Items bound to the collection.
3.) Observe the argument exception in question.
Expected behavior
The expectation is that ListBox classes should be able to display the items of both IList<T> and IReadOnlyList<T> and update their views as the collections change so long as they implement INotifyCollectionChanged
Screenshots
Not really relevant to this.
Desktop (please complete the following information):
Also not really relevant to this either.
Additional context
Could ItemsSourceView be modified around Line 49 to check before throwing an exception if Source is IReadOnlyList<object>? In my testing
_qc2 is IReadOnlyList<object>
returnedtrue
for instances of a class definitioninternal class QuickCollectionWithoutIList : IReadOnlyList<QuickModel>, INotifyCollectionChanged
But that's incompatible with the non-generic IList interface and can't be cast to IList properly. What a pain! And the
Inner
property of ItemsSourceView needs filling.I feel like this is perhaps stepping onto an earlier pain-point of the .Net BCL similar to the generic IEnumerable and the non-generic IEnumerable. 🙃
I suppose you could if you wanted to you could do some interesting castings, but I don't know if there are any IList implementing classes that don't also implement IReadOnlyList<T> - and all IReadOnlyList<T> are castable to IReadOnlyList<object> because of the rules of Covariance. But after checking the IList documentation there's plenty of types out there that implement IList but don't implement IReadOnlyList - what a pain!
Would it be too much to have some sort of interface of an indexing provider, that could have implementations that use either IList as a parameter, or IReadOnlyList<T>, and all it needs to do is provide a method of indexing the lists?
The text was updated successfully, but these errors were encountered: