Как создать компонент, позволяющий потребителю расширять интерфейсы для ввода параметров?

Это может быть глупый вопрос, но мои поиски до сих пор не увенчались успехом, и я беспокоюсь, что у меня может быть слишком конкретный вариант использования, чтобы найти четкий ответ где-либо еще.

Я работаю над созданием компонента 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 можно было каким-то образом расширить/унаследовать/преобразовать, чтобы потребитель компонента 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; }
}

К сожалению, хотя это кажется достаточно простой вещью, использование этого класса в Binding List в качестве входных данных для компонента просто не работает, потому что компонент ожидает интерфейс, и даже если класс реализует интерфейс, компилятор не может этого сделать. t конвертировать между ними.

Возможные решения, о которых я думал до сих пор, которые не сработали:

  1. Частичные интерфейсы: создайте частичный интерфейс в компоненте и другой такой же интерфейс в проекте с использованием компонента. Это не удается с самого начала, потому что частичные интерфейсы, как и в случае с классами, должны быть частью одной и той же сборки.
  2. Наследование и расширение интерфейса: создайте интерфейс в потребляющем проекте, который наследуется от интерфейса компонента, и используйте его. К сожалению, это также приводит к ошибке несоответствия типов во время компиляции.

Мне действительно кажется, что я упускаю что-то фундаментальное в том, как dotnet относится к такого рода вещам, но я также чувствую, что пытаюсь сделать что-то довольно простое и не слишком возмутительное, и это не должно быть так сложно, как я. м найти его. Если я ошибаюсь, пожалуйста, дайте мне знать, и в любом случае я был бы признателен за любые предложения о том, как сделать то, что я делаю лучше и/или по-другому, чтобы это действительно работало.

Обновлять

Кажется, мне лучше показать это, чем пытаться описать, так что вот рабочий пример того, чего я пытаюсь достичь.

Дело в том, что средство просмотра изображений заботится только об определенных свойствах класса PointOfInterest, в частности о свойстве position. Все остальные свойства будут иметь значение только за пределами средства просмотра изображений.

Обратите внимание, что этот код компилируется и работает правильно только потому, что я использую класс вместо интерфейса. Если бы я использовал этот интерфейс, я бы получил ошибку преобразования, которая полностью помешала бы компиляции кода.

🤔 А знаете ли вы, что...
С C# легко создавать графические приложения с помощью Windows Forms и WPF.


65
3

Ответы:

Я не думаю, что у меня достаточно информации, чтобы полностью понять, но, возможно, вы можете попробовать следующее

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

Решено

тл; ДР

Выяснил решение, используя ограничения универсального типа. В конечном счете, однако, у меня очень специфический вариант использования. Как показывает ответ Шона Кертиса, эта проблема обычно решается с помощью интерфейса для перечислимого типа, а не принудительного использования конкретного класса. Если вам это сойдет с рук, я рекомендую последовать его примеру.

Если вы просто пытаетесь понять, почему ваш объект List<MyClass> не будет привязан к вашему параметру List<IMyClass>, я бы порекомендовал вам проверить этот очень информативный и гораздо более полезный ответ.

Отвечать

Мы с коллегой проработали это вместе и нашли решение, включающее универсальные типы и ограничения типов. Это немного круговое движение, но оно работает именно так, как мне нужно. Вот рабочий пример.

Соответствующие части включали добавление соответствующим образом ограниченного параметра универсального типа в компонент средства просмотра изображений, например...

@typeparam TPointOfInterest where TPointOfInterest : IPointOfInterest

... и затем используя универсальный тип в соответствующем параметре компонента:

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

При этом средство просмотра изображений примет список привязок данного типа класса, хотя вам все равно нужно указать, какой класс использовать для ссылки на универсальный тип:

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

Это выполняет все, что мне нужно:

  • Позволяет компоненту просмотра изображений определять два очень простых интерфейса для своих входных параметров.
  • Позволяет приложению/родительскому компоненту реализовывать и расширять эти интерфейсы.
  • Поддерживает ссылку BindingList от родителя к дочернему (в моем случае это важно для обеспечения синхронизации списка между ними)

Это было раздражающе трудно взломать, учитывая то, что я не думаю, было особенно диковинной целью. Я надеюсь, что это в конечном итоге избавит кого-то еще от изжоги.