CropperBlazor / Cropper.Blazor

Cropper.js as Blazor component for cropping images
https://cropperblazor.github.io
MIT License
118 stars 12 forks source link

Replacing image results in non-functional cropper on first attempt and functional but duplicated cropper on second #332

Open NovaXeros opened 1 month ago

NovaXeros commented 1 month ago

Description: When attempting to replace an image using ReplaceAsync() functionality, the cropper component removes the previous image and displays the new one, but programmatic attempts to change aspect ratios are non-functional and the image size is incorrect.

If image replacement is attempted a second time, the previous incorrect cropper component is duplicated and pushed down the page and a new cropper component, with the correct image, size and functionality appears.

The original component remains on the page with the ability to move the selection square but non-functional otherwise.

All image replacements after this correctly replace the top-most cropper and all functionality on this cropper works, but the original erroneous cropper remains below.

Tech stack: .NET 8 Blazor Web App, in ServerInteractive render mode.

Replacement code:

    private async void UploadFiles(IBrowserFile file)
    {
        files.Clear();
        files.Add(file);

        if (file != null)
        {
            OldSrc = Src;
            string newSrc = await _cropperComponent!.GetImageUsingStreamingAsync(file, file.Size);

            if (IsErrorLoadImage)
            {
                IsAvailableInitCropper = true;
                IsErrorLoadImage = false;
            }
            else
            {
                IsAvailableInitCropper = false;
            }

            await Task.WhenAll(
                _cropperComponent!.ReplaceAsync(newSrc, false).AsTask()
            )
            .ContinueWith(x =>
            {
                Src = newSrc;
            });
        }
    }

Screenshots:

On the screenshots, the buttons below Upload Image are preset aspect ratios when clicked. These buttons are non functional on the first image replacement cropper, but work again after another replacement once the cropper is duplicated.

On initial load:

image

After first image upload:

image

After second image upload:

image

(Scrolled down to show the original "broken" cropper):

image

Recording: ezgif-3-441f9f7c1e

NovaXeros commented 1 month ago

Added a recording to better show the issue.

MaxymGorn commented 1 month ago

Hi @NovaXeros. Make sure you fill IsAvailableInitCropper parameter (possible other parameters for handling errors) in cropper component. Example: https://github.com/CropperBlazor/Cropper.Blazor/blob/dev/src/Cropper.Blazor/Client/Pages/Replace/Examples/BasicInputReplaceImageWithNewSizeExample.razor#L7

MaxymGorn commented 1 month ago

@NovaXeros just let me know if it helped you

NovaXeros commented 1 month ago

Hi @MaxymGorn

Thanks for the reply.

All parameters are set as per the example, apologies I didn't show the code for that on the original ticket:

<CropperComponent Class="cropper-example" 
                    Src="@Src"
                    OnLoadImageEvent="OnLoadImageEvent"
                    ErrorLoadImageSrc="@_errorLoadImageSrc"
                    ErrorLoadImageClass="cropper-error-load center"
                    IsAvailableInitCropper="@IsAvailableInitCropper"
                    IsErrorLoadImage="@IsErrorLoadImage"
                    OnErrorLoadImageEvent="OnErrorLoadImageEvent"
                    @ref="_cropperComponent"
                    Options="new Options()" />
    private CropperComponent? _cropperComponent = null!;
    IList<IBrowserFile> files = new List<IBrowserFile>();
    private bool AspectRatiosDisabled => files.Count == 0;
    private string Src = "img/cropperblazor.png";
    private string OldSrc = string.Empty;
    private string _errorLoadImageSrc = "img/cropperblazor.png";
    private bool IsErrorLoadImage { get; set; } = false;
    private bool IsAvailableInitCropper { get; set; } = true;

For posterity, here's the entire code implementation:

@code {
    private CropperComponent? _cropperComponent = null!;
    IList<IBrowserFile> files = new List<IBrowserFile>();
    private bool AspectRatiosDisabled => files.Count == 0;
    private string Src = "img/cropperblazor.png";
    private string OldSrc = string.Empty;
    private string _errorLoadImageSrc = "img/cropperblazor.png";
    private bool IsErrorLoadImage { get; set; } = false;
    private bool IsAvailableInitCropper { get; set; } = true;

    private struct AspectRatios
    {
        public static readonly decimal Table = 0.77m;
        public static readonly decimal Vertical = 0.62m;
        public static readonly decimal Horizontal = 1.25m;
        public static readonly decimal Square = 1m;
        public static readonly decimal Free = 0m;
    }

    private void SetAspectRatio(decimal ratio)
    {
        _cropperComponent!.SetAspectRatio(ratio);
        StateHasChanged();
    }

    private async void UploadFiles(IBrowserFile file)
    {
        files.Clear();
        files.Add(file);

        if (file != null)
        {
            OldSrc = Src;
            string newSrc = await _cropperComponent!.GetImageUsingStreamingAsync(file, file.Size);

            if (IsErrorLoadImage)
            {
                IsAvailableInitCropper = true;
                IsErrorLoadImage = false;
            }
            else
            {
                IsAvailableInitCropper = false;
            }

            await Task.WhenAll(
                _cropperComponent!.ReplaceAsync(newSrc, false).AsTask()
            )
            .ContinueWith(x =>
            {
                Src = newSrc;
            });
        }
    }

    public void OnErrorLoadImageEvent(ErrorEventArgs errorEventArgs)
    {
        IsErrorLoadImage = true;
        Destroy();
        StateHasChanged();
    }

    public void Destroy()
    {
        _cropperComponent?.Destroy();
        _cropperComponent?.RevokeObjectUrlAsync(Src);
    }

    public async void OnLoadImageEvent()
    {
        if (!string.IsNullOrWhiteSpace(OldSrc))
        {
            await _cropperComponent!.RevokeObjectUrlAsync(OldSrc);
        }
    }
}
NovaXeros commented 3 weeks ago

@MaxymGorn is there any additional info you need from me in order to investigate this? Want me to upload my project to a repo?

MaxymGorn commented 3 weeks ago

@NovaXeros Sounds good

NovaXeros commented 3 weeks ago

@MaxymGorn I've added you as a collab to the following private repo:

https://github.com/NovaXeros/VotVImageUploader

In there you will find the entire VS solution uploaded. Hope that helps narrow down the issue.

MaxymGorn commented 3 weeks ago

@NovaXeros ok, I accept your repo invite. I plan to go through it this week.

MaxymGorn commented 3 weeks ago

@NovaXeros I took a quick look at the code. My main concern is that this is Server Interactive mode on the page. Maybe you should choose another mode with auto/client interaction. So far, no detailed research has been done on the cropper component and the new render mods with .net 8

NovaXeros commented 3 weeks ago

@MaxymGorn

I did already try this with Auto, Server and WebAssembly.

Server results in the reported behaviour. Auto and WebAssembly results in zero functionality from the get-go and the following error in the web page debugger:

Error: One or more errors occurred. (Root component type 'VotVImageUploader.Components.Pages.Home' could not be found in the assembly 'VotVImageUploader'.) at Jn (marshal-to-js.ts:349:18) at Ul (marshal-to-js.ts:306:28) at 00b1eed6:0x1facb at 00b1eed6:0x1bf8c at 00b1eed6:0xf173 at 00b1eed6:0x1e7e5 at 00b1eed6:0x1efdb at 00b1eed6:0xcfed at 00b1eed6:0x44108 at e. (cwraps.ts:338:24) callEntryPoint @ blazor.web.js:1 await in callEntryPoint (async) ei @ blazor.web.js:1 await in ei (async) Zr @ blazor.web.js:1 startWebAssemblyIfNotStarted @ blazor.web.js:1 resolveRendererIdForDescriptor @ blazor.web.js:1 determinePendingOperation @ blazor.web.js:1 refreshRootComponents @ blazor.web.js:1 (anonymous) @ blazor.web.js:1 setTimeout (async) rootComponentsMayRequireRefresh @ blazor.web.js:1 startLoadingWebAssemblyIfNotStarted @ blazor.web.js:1

My (albeit limited) understanding of the new .NET 8 Interactive page modes is that InteractiveServer 'replicates' the behaviour of the old .NET <8 Blazor Server projects, InteractiveWebAssembly replicates the behaviour of the old .NET <8 Blazor WebAssembly projects and InteractiveAuto decides whether to run the page as Server or WA depending on if the consumer has visited the page before (ie., renders as Server-mode first time, downloads the WA version of the page for future visits).

NovaXeros commented 3 weeks ago

@MaxymGorn

I've resolved the above issue regarding the error in the debug and total non-functioning in WebAssembly and Auto. For reference, this appears to have been due to the page being loaded in the Server project rather than the client.

However, I can confirm that the same behaviour exists across all InteractiveServer, InteractiveAuto and InteractiveWebAssembly versions.

I've just pushed a change to the repo that now splits the functionality into 3 pages with the different rendering modes to demonstrate that the error exists across all 3.

MaxymGorn commented 3 weeks ago

@NovaXerosI available to reproduce this issue, if you urgently need similar functionality, I can offer to use full rebuild cropper: https://cropperblazor.github.io/examples/rebuild#simple-usage

MaxymGorn commented 3 weeks ago

@NovaXeros Now, I know the reason: IsAvailableInitCropper is somehow not changed for some reason, although in the code it seems to say changed

image

but in demo site it works properly. I suppose it a bug or specific rendering in new .net 8 sdk (blazor web js)

MaxymGorn commented 3 weeks ago

@NovaXeros I figure out that: In the new .NET 8, the blazor.web.js SDK parameters components are not updated timely. I strongly recommend using StateHasChanged() method after changing a component's parameter, but be careful with this because you're updating the entire component where there may be more than one component, which can also affect performance a bit when there are too many components on the page or animations. as an option, make a separate wrapper with minimal functionality in this case.

Decision in your case:

image
MaxymGorn commented 3 weeks ago

@NovaXeros please let me know if offered approach works for you!

NovaXeros commented 3 weeks ago

@MaxymGorn

Just confirming that the approach of adding StateHasChanged() after changing IsAvailableInitCropper has indeed resolved the issue. Noted your comments about potential performance issues, thankfully this is for a very small application with very limited components and so isn't a consideration here, but good to know for any others who may be facing this!

Glad to hear you've identified the root cause also, I'll be keen to see how the final resolution works. I'd take a look myself, but I'm afraid JS isn't my strong suit!

Many thanks for your assistance and time.