您如何创建一个允许消费者扩展参数输入接口的组件?

How do you create a component that allows the consumer to extend the interfaces for parameter inputs?

这可能是个愚蠢的问题,但到目前为止,我的搜索结果很短,我担心我的用例可能过于具体,无法在其他任何地方找到明确的答案。

我正在创建一个使用绑定列表作为输入参数的 Blazor 组件。这是为了让组件可以从组件外部检测到列表的更改,并做出相应的响应。该列表实现了列表项的接口,因此:

[Parameter]
public BindingList<IPointOfInterest> PointsOfInterest { get => ...; set => ...; }

同时,界面如下所示:

public interface IPointOfInterest
{
    public float[] position { get; set; }
}

从那里我将其实例化为父对象的 属性,如下所示:

var currentImage = new Image { DataUri = $"images/image-001.jpg", PointsOfInterest = new BindingList<IPointOfInterest>() };

然后根据当前选择的对象,我简单地将它绑定到组件,如下所示:

<ImageViewer Image="@currentImage"
             PointsOfInterest="@currentImage.PointsOfInterest">
</ImageViewer>

我的目标是让 IPointOfInterest 接口可以 extended/inherited from/transmogrified 以某种方式让 Blazor 组件的使用者可以根据需要添加自己的属性并利用相同代码中的那些,而无需维护单独的对象。一个例子可能是这样的:

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

不幸的是,虽然这看起来很简单,但在绑定列表中使用这个 class 作为组件的输入根本不起作用,因为组件需要接口,即使class实现了接口,编译器无法在两者之间进行转换。

到目前为止我想到但没有奏效的可能解决方案:

  1. 部分接口:在组件中创建一个部分接口,在使用该组件的项目中创建另一个相同的接口。这立即失败了,因为部分接口,与 classes 一样,必须是同一程序集的一部分。
  2. 继承和扩展接口:在继承自组件接口的消费项目中创建一个接口,并使用它。不幸的是,这也会在编译时出现类型不匹配错误。

我确实觉得我缺少一些关于 dotnet 如何处理这些类型的东西的基本知识,但我也觉得我正在尝试做一些非常简单而不是非常离谱的事情,它不应该是和我发现的一样困难。如果我对此有误,请务必告诉我,无论如何,对于如何以实际可行的方式更好地处理我正在做的事情的任何建议,我将不胜感激and/or。

更新

看来我最好展示它而不是试图描述它,所以 here's a working example of what I'm trying to achieve

这里的重点是图像查看器只关心 PointOfInterest class 上的某些属性 - 特别是 position 属性。所有其他属性只在图像查看器 之外 有影响。

请注意,此代码能够正确编译和运行只是因为我利用了 class 而不是接口。如果我要使用该接口,我会得到一个转换错误,这将阻止代码完全编译。

我认为我没有足够的信息来完全理解,但也许你可以尝试以下方法

<ImageViewer Image="@currentImage"
             PointsOfInterest="@(currentImage.PointsOfInterest as BindingList<IPointOfInterest>)">
</ImageViewer>

或者显式或隐式强制转换的方法也不错。

您应该能够使用接口和泛型。这是一个演示:

数据类和接口

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

组件:

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

}

和演示页面:

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

TL;博士

想出了一个使用通用类型约束的解决方案。不过,归根结底,我的是一个非常具体的用例。正如 Shawn Curtis 的回答所说明的那样,这个问题通常通过使用可枚举类型的接口来解决,而不是强制使用具体的 class。如果你能侥幸逃脱,我建议以他为榜样。

如果您只是想弄清楚为什么您的 List<MyClass> object 不会绑定到您的 List<IMyClass> 参数,我建议您查看 .

回答

我和一位同事最终一起解决了这个问题,并提出了一个涉及泛型类型和类型约束的解决方案。这有点迂回,但它的工作方式几乎完全符合我的需要。 Here is a working example.

相关部分涉及向图像查看器组件添加适当约束的泛型类型参数,如下所示...

@typeparam TPointOfInterest where TPointOfInterest : IPointOfInterest

...然后在对应的组件参数中使用泛型:

[Parameter]
public BindingList<TPointOfInterest> PointsOfInterest { get ; set ; }

这样,图像查看器将接受给定 class 类型的绑定列表,尽管您仍然必须告诉它 class 用于通用类型引用的内容:

<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"
            },
        }
    };
}

这完成了我需要的一切:

  • 允许图像查看器组件为其输入参数定义两个非常基本的接口
  • 允许 application/parent 组件在这些接口上实现和扩展
  • 维护从 parent 到 child 的 BindingList 引用(在我的情况下对于确保列表可以在两者之间保持同步至关重要)

考虑到我认为不是特别古怪的东西 objective,破解起来非常困难。我希望这最终能让其他人免于胃痛。