Search code examples
c#dotvvm

DotVVM - Code only component DataSource property is null when collection is passed in


I have my own Accordion code-only component

Here is my view where I have repeater which loads list of article sections. Each article section have list of articles. So with that I want to archieve that every article section will have his own accordion, which will contain articles. Thats why I have it in repeater

<div class="box box-primary">
    <dot:Repeater DataSource="{{value: AccordionList}}">
        <ItemTemplate>
            <coc:Accordion DataSource="{{value: Articles}}"></coc:Accordion>
        </ItemTemplate>
    </dot:Repeater>       
</div>

Accordion code-only component. My DataSource is always null even when I clearly see, that AccordionList contains List of Articles which is never null, but is never passed into my DataSource. When I change type of AccordionList to ArticleListDTOand pass it directly into my Accordion component, it worked well, but thats not what I want.

public class Accordion : HtmlGenericControl
{
    public Accordion() : base("div")
    {
    }
    public static readonly DotvvmProperty DataSourceProperty;
    static Accordion()
    {
           DataSourceProperty = DotvvmProperty.Register<List<ArticleListDTO>, Accordion>(c=>c.DataSource); 
    }
    //DataSource is always null
    public List<ArticleListDTO> DataSource
    {
        get => (List<ArticleListDTO>)GetValue(DataSourceProperty);
        set => SetValue(DataSourceProperty, value);
    } 

    protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
    {
        Attributes.Add("class", "accordion");

        base.AddAttributesToRender(writer, context);
    }

    public void DataBind(IDotvvmRequestContext context)
    {
        Children.Clear();
        foreach (var item in DataSource)
        {
            DataBindItem(this, item, context);
        }
    }....etc

ViewModel

public List<ArticleSectionListDTO> AccordionList { get; set; } = new List<ArticleSectionListDTO>();
public List<ArticleSectionListDTO> AccordionListUnsorted { get; set; } = new List<ArticleSectionListDTO>();

protected override void OnItemLoading()
{
    AccordionListUnsorted = Task.Run(() => articleSectionFacade.GetAllNotModifiedArticleSections()).Result;

    AccordionList = Task.Run(() => articleSectionFacade.CreateTree(AccordionListUnsorted, null)).Result.ToList();
}

DTOs - I deleted rest of properties to make it clear

public class ArticleListDTO
{
    public string Name { get; set; }

    public int? ParentArticleId { get; set; }
    public bool HasCategories => AssignedToArticle?.Count > 0;
    public List<ArticleListDTO> AssignedToArticle { get; set; }
    //Can contain sub articles
    public List<ArticleListDTO> Articles { get; set; } = new List<ArticleListDTO>();
}

public class ArticleSectionListDTO : ListDTO
{
    public string Name { get; set; }

    public int? ParentArticleSectionId { get; set; }
    public bool HasCategories => AssignedToMenuItem?.Count > 0;
    public List<ArticleSectionListDTO> AssignedToMenuItem { get; set; }
    public List<ArticleListDTO> Articles { get; set; } = new List<ArticleListDTO>();
}

Solution

  • The problem is that Repeater probably uses the client-rendering mode (it's the default). When it renders the HTML, it renders something like this:

    <div data-bind="foreach: something">
        <!-- template -->
    </div>
    

    When the template is rendered, its DataContext is null (becasue the template must not contain data from an item - it is a template).

    So you have two options here:

    1. Turn on server rendering by adding RenderSettings.Mode="Server" to the Repeater.
    2. Update your control so it doesn't call DataBind when DataContext is null.