'How do you create a component that allows the consumer to extend the interfaces for parameter inputs?
This may be a dumb question, but my search so far has come up short and I worry I might have too specific of a use case to find a clear answer anywhere else.
I am working on creating a Blazor component that utilizes a Binding List as an input parameter. This is specifically so the component can detect changes to the list from outside the component, and respond accordingly. The list implements an interface for list items thusly:
[Parameter]
public BindingList<IPointOfInterest> PointsOfInterest { get => ...; set => ...; }
The interface, meanwhile, looks like this:
public interface IPointOfInterest
{
public float[] position { get; set; }
}
From there I instantiate it as a property of a parent object , like this:
var currentImage = new Image { DataUri = $"images/image-001.jpg", PointsOfInterest = new BindingList<IPointOfInterest>() };
And then based on the currently selected object I simply bind it to the component like this:
<ImageViewer Image="@currentImage"
PointsOfInterest="@currentImage.PointsOfInterest">
</ImageViewer>
My goal here is to make it so that the IPointOfInterest
interface could be extended/inherited from/transmogrified somehow so that the consumer of the Blazor component could tack on their own properties as desired and leverage those in the same code without having to maintain separate objects. An example might be something like this:
public class PointOfInterest : IPointOfInterest
{
public float[] position { get; set; }
public int id { get; set; }
public string name { get; set; }
public string description { get; set; }
public float entityId { get; set; }
}
Unfortunately, while this seems like a simple enough thing, using this class in a Binding List as an input to the component simply doesn't work, because the component expects the interface, and even though the class implements the interface, the compiler can't convert between the two.
Possible solutions I've thought of so far which have not worked:
- Partial interfaces: create a partial interface in the component and another of the same interface in the project using the component. This fails right off the bat because partial interfaces, as with classes, have to be a part of the same assembly.
- Inheriting from and extending the interface: create an interface in the consuming project that inherits from the component's interface, and use that. Unfortunately this also presents a type mismatch error at compile time.
I do feel as though I'm missing something fundamental about how dotnet treats these types of things, but I also feel as though I'm trying to do something pretty straightforward and not terribly outrageous and it ought not to be as difficult as I'm finding it. If I'm wrong about that, please do let me know, and in any case I'd appreciate any suggestions about how to go about what I'm doing better and/or differently in a way that actually works.
Update
It seems I'm way better off showing this than trying to describe it, so here's a working example of what I'm trying to achieve.
The point here is that the Image Viewer only cares about certain properties on the PointOfInterest
class - specifically the position
property. All other properties would only matter outside of the Image Viewer.
Note that this code compiles and runs properly only because I'm leveraging the class instead of the interface. If I were to use the interface, I would get a conversion error that would prevent the code from compiling altogether.
Solution 1:[1]
TL; DR
Figured out a solution using generic type constraints. Ultimately, though, mine is a very specific use case. As Shawn Curtis's answer illustrates, this problem is generally solved by using an interface for the enumerable type, rather than forcing the use of a concrete class. If you can get away with it, I recommend following his example.
If you're just here trying to figure out why your List<MyClass>
object won't bind to your List<IMyClass>
parameter, I'd recommend you check out this very informative and much more helpful answer.
Answer
A colleague and I ended up working through this together and coming up with a solution involving generic types and type constraints. It's a bit roundabout, but it works pretty much exactly the way I needed it to. Here is a working example.
The relevant parts involved adding an appropriately constrained generic type parameter to the image viewer component, like so...
@typeparam TPointOfInterest where TPointOfInterest : IPointOfInterest
...and then using the generic type in the corresponding component parameter:
[Parameter]
public BindingList<TPointOfInterest> PointsOfInterest { get ; set ; }
With that, the image viewer will accept a binding list of the given class type, though you do still have to tell it what class to use for the generic type reference:
<ImageViewer Image="@currentImage"
PointsOfInterest="@currentImage.PointsOfInterest"
TPointOfInterest="PointOfInterest">
</ImageViewer>
@code
{
public Image currentImage = new Image()
{
DataUri = "https://www.longimageurl.com/boy/this/is/an/awfully/long/url/for/just/one/image.jpg",
PointsOfInterest = new BindingList<PointOfInterest>() {
new PointOfInterest() {
position = new float[] { 1, 2, 3 },
id=1,
name="POI #1",
description="FOO"
},
new PointOfInterest() {
position = new float[] { 4, 5, 6 },
id=1,
name="POI #2",
description="BAR"
},
}
};
}
This accomplishes everything I needed it to:
- Allows the image viewer component to define two very basic interfaces for its input parameters
- Allows the application/parent component to implement and expand on those interfaces
- Maintains the
BindingList
reference from parent to child (critical in my case for ensuring the list can stay synchronized between the two)
This was annoyingly difficult to crack, given what I don't think was a particularly outlandish objective. I hope this ends up sparing someone else some heartburn.
Solution 2:[2]
You should be able to use interfaces and generics. Here's a demo:
Data classes and Interface
public interface IPointOfInterest
{
public decimal Lat { get; }
public decimal Long { get; }
}
public class DataA : IPointOfInterest
{
public decimal Lat { get; set; }
public decimal Long { get; set; }
}
public class DataB : IPointOfInterest
{
public decimal Lat { get; set; }
public decimal Long { get; set; }
}
public class DataC
{
public decimal Lat { get; set; }
public decimal Long { get; set; }
}
Component:
@typeparam TItem
<h3>PointComponent</h3>
@if (list is not null)
{
@foreach (var item in list)
{
<div class="p-2 row">
<div class="col-2">
Lat: @item.Lat
</div>
<div class="col-2">
Long: @item.Long
</div>
</div>
}
}
else
{
<div class="p2">No Data</div>
}
@code {
[Parameter] public IEnumerable<TItem>? DataList { get; set; }
private IEnumerable<IPointOfInterest>? list
{
get
{
if (DataList is not null && DataList is IEnumerable<IPointOfInterest>)
return new List<IPointOfInterest>(DataList!.Cast<IPointOfInterest>());
return null;
}
}
}
And Demo page:
@page "/"
<PageTitle>Index</PageTitle>
<PointComponent DataList=dataA />
<PointComponent DataList=dataB />
<PointComponent DataList=dataC />
@code {
public IEnumerable<DataA> dataA = new List<DataA>
{
new DataA {Lat = 20, Long=10 },
new DataA {Lat = 20, Long=10 }
};
public IEnumerable<DataB> dataB = new List<DataB>
{
new DataB {Lat = 20, Long=10 },
new DataB {Lat = 20, Long=10 }
};
public IEnumerable<DataC> dataC = new List<DataC>
{
new DataC {Lat = 20, Long=10 },
new DataC {Lat = 20, Long=10 }
};
}
Solution 3:[3]
I don't think I have enough information to fully understand, but maybe you can try the following
<ImageViewer Image="@currentImage"
PointsOfInterest="@(currentImage.PointsOfInterest as BindingList<IPointOfInterest>)">
</ImageViewer>
Or an approach with an explicit or implicit cast could be good too.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Dumas.DED |
Solution 2 | MrC aka Shaun Curtis |
Solution 3 | Dylan Barquilla |