ant-design-blazor icon indicating copy to clipboard operation
ant-design-blazor copied to clipboard

Table does not work when item type is an interface

Open GeertvanHorrik opened this issue 3 years ago • 8 comments

Describe the bug

I tried binding a list of interfaces like this:

public List<IJob> Items { get; private set; }

Blazor

<Table TItem="IJob"
        DataSource="@Items">
    <Column Title="Name"
            Field="@context.Name" />
</Table>

Whenever I change the TItem to a class (Job), it starts working.

Exceptions (if any)

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Exception has been thrown by the target of an invocation.
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.TypeInitializationException: The type initializer for 'AntDesign.Table`1' threw an exception.
 ---> System.MemberAccessException: Cannot access member.
   at System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObjectInternal(Type type)
   at System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type type)
   at AntDesign.Table`1[[IJob, MyProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]..cctor()
   --- End of inner exception stack trace ---
   at System.Reflection.RuntimeConstructorInfo.InternalInvoke(Object obj, Object[] parameters, Boolean wrapExceptions)
   --- End of inner exception stack trace ---
   at System.Reflection.RuntimeConstructorInfo.InternalInvoke(Object obj, Object[] parameters, Boolean wrapExceptions)
   at System.RuntimeType.CreateInstanceMono(Boolean nonPublic, Boolean wrapExceptions)
   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean wrapExceptions, Boolean skipCheckThis, Boolean fillCache)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic)
   at System.Activator.CreateInstance(Type type)
   at Microsoft.AspNetCore.Components.DefaultComponentActivator.CreateInstance(Type componentType)
   at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateComponent(Type componentType)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame& frame, Int32 parentComponentId)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()

Further technical details

  • AntDesign Nuget Package version

0.6.0-nightly-2101210934

  • Include the output of dotnet --info
Microsoft.NETCore.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

GeertvanHorrik avatar Jan 21 '21 21:01 GeertvanHorrik

Thank you for contacting us @GeertvanHorrik

That's right, you can't use the interface. That's a limitation of c#. I wonder what your thoughts are on this?

ElderJames avatar Jan 22 '21 02:01 ElderJames

I was wondering why it is creating an instance of the model. The data source already contains an instance of the model (so should work against C#).

Could you explain why it's trying to do this? I think property expressions, reflections, etc all work against interface, as long as you (re)use the existing model instances.

GeertvanHorrik avatar Jan 22 '21 09:01 GeertvanHorrik

This is related to our design of Column, need to pass an instance of TItem to it.

https://github.com/ant-design-blazor/ant-design-blazor/blob/efaf3854ef5f73a88d2c2dca74873f5bb711dfff/components/table/Table.razor#L22

But with the addition of the DataIndex feature, it is possible to do this in the future.

ElderJames avatar Jan 31 '21 14:01 ElderJames

Hopefully, this is fixed soon as I'm using interfaces due to decoupling.

hailstorm75 avatar Mar 01 '21 13:03 hailstorm75

@Zonciu cc

ElderJames avatar Mar 01 '21 14:03 ElderJames

Table's design is not good, it needs to be redesigned to achieve this.

For workaround, you can use Decorator Pattern, create a table model class that implemented IJob then wrap all IJob instances. Or you can copy all data to the table model class instance

Decorator pattern example:

<Table TItem="JobModel" DataSource="TableData">
    <Column Title="Name" TData="string" Field="context.Name"></Column>
    <Column Title="Progress" TData="int" @bind-Field="context.Progress"></Column>
    <Column Title="Time" TData="DateTime" DataIndex="Time"></Column>
</Table>

@code {

    public List<JobModel> TableData = new();

    public List<RealJob> Jobs;

    protected override void OnInitialized()
    {
        Jobs = new()
        {
            new RealJob() {Name = "AAA", Progress = 16, Time = DateTime.Today - TimeSpan.FromDays(4)},
            new RealJob() {Name = "BBB", Progress = 61, Time = DateTime.Today - TimeSpan.FromDays(3)},
            new RealJob() {Name = "CCC", Progress = 89, Time = DateTime.Today - TimeSpan.FromDays(2)}
        };
        TableData.AddRange(Jobs.Select(job => new JobModel(job)));
    }

    public interface IJob
    {
        string Name { get; }

        int Progress { get; set; }

        DateTime Time { get; set; }
    }

    public class RealJob : IJob
    {
        public string Name { get; set; }

        public int Progress { get; set; }

        public DateTime Time { get; set; }
    }

    public class JobModel : IJob
    {
        public IJob Instance;

        public JobModel(IJob job)
        {
            Instance = job;
        }

        public string Name
        {
            get => Instance?.Name ?? default;
        }

        public int Progress
        {
            get => Instance?.Progress ?? default;
            set => Instance.Progress = value;
        }

        public DateTime Time
        {
            get => Instance?.Time ?? default;
            set => Instance.Time = value;
        }
    }

}

Zonciu avatar Mar 01 '21 17:03 Zonciu

Any updates on this?

I am also exploring AntBlazor in a new project that uses interfaces to represent data models which are generated from GraphQl queries and mutations (https://chillicream.com/docs/strawberryshake/), but these interfaces can't be used easily with this library.

Wrapping these interfaces with classes using a decorator pattern isn't an option because it takes away the advantage of using Strawberry Shake (and GraphQl, since the schema may change quite frequently).

MarkoH17 avatar Sep 20 '22 03:09 MarkoH17

@MarkoH17 Sorry, this is a component design limitation. We rely too much on the Column to render some has nothing to do with RenderFargment content. We hope someone can improve that.

ElderJames avatar Sep 20 '22 03:09 ElderJames