sion_corn
sion_corn

Reputation: 3151

blazor force full render instead of differential render

I am trying to display images using this masonry js package but I am having difficulties.

I have an image upload form. The user clicks a button to bind images to Model.Images, and then they get displayed on the page. The user can bind multiple batches of images before submitting the form. Each time the user binds another batch of images to Model.Images, the previous batch is persisted - that is to say, I do not truncate Model.Images before binding the images from the next batch.

The problem I am seeing is: the masonry js script needs to be re-triggered every time Model.Images gets added to, because the js only applies to the most recently-rendered view. However, it seems every time I force StateHasChanged(); the images from the most recently-added batch overlap the previous images, so I see this:

after first batch of images is bound after first batch of images is bound

after second batch of images is bound after second batch of images is bound

I think this has something to do with differential rendering. If I move the images to a helper object, truncate Model.Images, force StateHasChanged(), and then repopulate Model.Images, the images get displayed side by side properly. I most likely am just approaching this incorrectly. Can someone advise me how to achieve what I want without my hacky implementation? I pasted a simplified version of my code below - this the implementation that is producing the bad results pictured above.

razor component (the TriggerMasonry() event gets called after Model.Images gets appended to.)

@for (int i = 0; i < Model.Images.Count; j++)
{
    var iCopy = i; //see https://stackoverflow.com/a/56426146/323447
    var uri = $"data:{Model.Images[iCopy].ImageMimeType};base64,{Convert.ToBase64String(Model.Images[iCopy].ImageDataLarge.ToArray())}";

    <div class="grid-item" @ref=MasonryElement>
        <div class="card m-2 shadow-sm" style="position:relative;">
            <img src="@uri" class="card-img-top" alt="..." loading="lazy">
        </div>
    </div>
}


@code {
    ElementReference MasonryElement;

    private async void TriggerMasonry()
    {
        if (Model.Images.Where(x => x.ImageData != null).Any())
        {
            StateHasChanged();
            await JSRuntime.InvokeVoidAsync("initMasonryDelay", MasonryElement);
            StateHasChanged();
        }
    }
}

js

function initMasonryDelay() {

    setTimeout(function () {

        // init Masonry
        var $grid = $('.grid').masonry({
            itemSelector: '.grid-item',
            percentPosition: true,
            columnWidth: '.grid-sizer'
        });

        // layout Masonry after each image loads
        $grid.imagesLoaded().progress(function () {
            $grid.masonry();
        });

    }, 150); //milliseconds
}

css

* {
    box-sizing: border-box;
}

.grid:after {
    content: '';
    display: block;
    clear: both;
}

.grid-sizer,
.grid-item {
    width: 33.333%;
}

@media (max-width: 575px) {
    .grid-sizer,
    .grid-item {
        width: 100%;
    }
}

@media (min-width: 576px) and (max-width: 767px) {
    .grid-sizer,
    .grid-item {
        width: 50%;
    }
}

/* To change the amount of columns on larger devices, uncomment the code below */
@media (min-width: 768px) and (max-width: 991px) {
    .grid-sizer,
    .grid-item {
        width: 33.333%;
    }
}

@media (min-width: 992px) and (max-width: 1199px) {
    .grid-sizer,
    .grid-item {
        width: 25%;
    }
}

@media (min-width: 1200px) {
    .grid-sizer,
    .grid-item {
        width: 20%;
    }
}


.grid-item {
    float: left;
}

    .grid-item img {
        display: block;
        max-width: 100%;
    }

----Another example for user @Brian_Parker---- I tried to apply your suggestions but I'm still not having any luck. I clearly must not understand how JS interoperability works with blazor components. I think my problem has to do with not getting how @ref works. I sized the array like you suggested, but I'm just creating a proper-length array filled with null refs. I realize this must be wrong, but I am not sure what to fill them with - I tried replacing object[] imageRefs with a ElementReference[] imageRefs and filling imageRefs with ElementReference[i].Id and passing that to the JS function, but it didn't change the overall behavior of the page. I reverted to baseline for the sake of making my code easier to digest.

I pasted below a different example, that might be easier to understand in a UI context. If you could help me understand what I'm doing wrong here, I'd greatly appreciate it:

I have another component in which I'm trying to apply the Masonry JS to a dynamically-generated list of images. Basic overview: On load, 10 images are pulled from the server and rendered. When the user clicks a "load more" button, 10 more images are pulled from the server and get rendered. I am seeing the same issue where the 2nd round of images is laid over the first round.

My razor component:

<!-- Masonry -->
<div class="grid">
    <div class="grid-sizer"></div>
    @for (int i = 0; i < pagedThings.Data.Count; i++)
    {
        var iCopy = i; //see https://stackoverflow.com/a/56426146/323447
        <div @key="@pagedThings.Data[iCopy]" class="grid-item" @ref=imageRefs[iCopy]>
            <div class="card">
                @{ 
                    var mimeType = pagedThings.Data[iCopy].ImageThings.FirstOrDefault().ImageMimeTypeLarge;
                    var imgData = pagedThings.Data[iCopy].ImageThings.FirstOrDefault().ImageDataLarge;
                }
                <img src="@String.Format("data:" + mimeType + ";base64,{0}", Convert.ToBase64String(imgData))" loading="lazy">
            </div>
        </div>
    }
</div>

<!-- Load more button -->
<button class="btn" type="button" @onclick="GetMoreThings">
    <span>Load more</span>
</button>

@code {
    PagedResponse<List<Thing>> pagedThings { get; set; }
    private int currentPage = 1;
    private int currentPageSize = 10;
    object[] imageRefs;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            //load Things
            await LoadThings();
        }
        await base.OnAfterRenderAsync(firstRender);
    }

    private async Task LoadThings()
    {
        pagedThings = await ThingService.GetByPageAsync($"api/Things/GetThingsByPage?pageNumber={currentPage}&pageSize={currentPageSize}");
        imageRefs = new object[pagedThings.Data.Count()];
        await JSRuntime.InvokeVoidAsync("masonryAppendDelay", imageRefs[imageRefs.Length - 1]);
        StateHasChanged();
    }

    private async Task GetMoreThings()
    {
        currentPage++;

        var newData = await ThingService.GetByPageAsync($"api/Things/GetThingsByPage?pageNumber={currentPage}&pageSize={currentPageSize}");
        foreach (var item in newData.Data)
        {
            pagedThings.Data.Add(item);
        }
        imageRefs = new object[pagedThings.Data.Count()];
        await JSRuntime.InvokeVoidAsync("masonryAppendDelay", imageRefs[imageRefs.Length - 1]);
        StateHasChanged();
    }
}

My JS:

function masonryAppendDelay(element) {

    setTimeout(function () {

        // init Masonry
        var $grid = $('.grid').masonry({
            itemSelector: element,
            percentPosition: true,
            columnWidth: '.grid-sizer'
        });

        // layout Masonry after each image loads
        $grid.imagesLoaded().progress(function () {
            $grid.masonry();
            //$grid.masonry('layout');
        });

    }, 200); //milliseconds
}

My css is the same as in the original post.

Upvotes: 1

Views: 795

Answers (1)

Brian Parker
Brian Parker

Reputation: 14613

You are capturing a reference in loop. This will only assign the final loops reference. You will have to size an array to capture the references once you know who many iterations there is. refs = new object[someValue]

Your Trigger masonry function will then have to traverse the reference list.

<div @key="@Model.Images[iCopy]" class="grid-item" @ref=refs[iCopy]>
        <div class="card m-2 shadow-sm" style="position:relative;">
            <img src="@uri" class="card-img-top" alt="..." loading="lazy">
        </div>
</div>


@code { 
    object[] refs;
}

Call your java script for each reference after it has been rendered.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        foreach (var obj in refs)
        {
            await JSRuntime.InvokeVoidAsync("masonryAppendDelay", obj);
        }
    }
}

Upvotes: 4

Related Questions