dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Expose location changing event for NavigationManger #14962

Closed Postlagerkarte closed 2 years ago

Postlagerkarte commented 5 years ago

The NavigationManager exposes

LocationChanged | An event that fires when the navigation location has changed.

but it does not expose a LocationChangingevent.

Such an event that occurs before navigation takes place is very useful because it allows the previous view to prepare for its deactivation, for example, to display a warning, cancel navigation or silently save changes.

Can you provide such a feature or is there already another way to achieve the above?

javiercn commented 5 years ago

Thank you for your feature request @Postlagerkarte.

We'll consider this feature during the next release planning period and update the status of this issue accordingly.

Tiberriver256 commented 4 years ago

Not sure if this is what the OP was after with LocationChanging but it would be nice to have LocationChangeStart and LocationChangeEnd events. The use case for me would be to display a little progress bar to the user while navigation is happening.

DanielHWe commented 4 years ago

It would be nice to cancel the navigation to. Use Case is that the user has unsaved changes and show an error to him so he is not leaving without save.

Postlagerkarte commented 4 years ago

I hope that the team decides to give us a full set of methods, properties, and events to support navigation including easy access to the navigation history.

Such an improved version of the NavigationManager would certainly allow tracking the lifetime of a navigation via events like:

Navigating
Navigated
NavigationProgress
NavigationFailed
NavigationStopped

Basically, hoping for an improved/adjusted version of the NavigationService 😃

ShaunCurtis commented 4 years ago

Yes, it's great being able to develop an SPA application, but when the user can navigate away with changes unsaved, and no way to warn them, that's a serious flaw. You can use the javascript window.history.forward() trick in the _Hosts.html file to force the user back, but that refreshes the page losing all unsaved changes. For critical editing, I'm starting to use a state service to preserve changes, but it's a bit of a cludge to get round the problem.

aaronhudon commented 4 years ago

If anyone can post suggestions on how best to implement this in SSB without waiting for Microsoft, please do so here.

lucianotres commented 4 years ago

And would be nice to have a way to handle history stack or just a simple "location.replace(url);" Only way I found to do this currently, was with JS interop. js like this:

window.controles_interop = {
    ...
    location_replace: function (url) {
        window.location.replace(url);
    }
};

then call: await JSRuntime.InvokeVoidAsync("controles_interop.location_replace", $"/protocolo/{_id.Value}");

I mean something like this, so more practical: _nav.NavigateTo($"/protocolo/{_id.Value}", replace: true);

javiercn commented 4 years ago

@lucianotres We are adding that in a future update I think, but you don't need anything special to do that today, you can simply call JSRuntime.InvokeAsync("location.replace", url) and that will work

OlegQu commented 4 years ago

Are there any news about this issue's status? May be there are some possibilities to override or extend functions from EventDelegator.ts or NavigationManager.ts or use own NavigationManager.cs realization with scripts duplicating above default? Sorry, if this question sounds kind of... elementary - i'm still novice at coding.

ShaunCurtis commented 4 years ago

Hi, I've been flat out on a project where I've resolved most of the issues I had. I'm just finishing testing and hoping to publish something covering the subject later this week/next week. My basic approach has been:

  1. Use the window.onbeforeunload event to warn the user that they are leaving the "application". You don't much control over this, but it works and users I have tested it on got the message. Place it in the _Host.cshtml.
  2. Use a scoped data service class to track the condition (dirty/clean) of the edited record.
  3. Use a scoped user session service class to temporarily hold page routing data while transitioning between routes. This also helps on the transition from New to Edit when first saving a record.
  4. Customize the standard router to check the save state of the record and either navigate or instruct the current page that the user is trying to leave and an "Are You Sure/Unsaved Data" action is required. Note that the NavigationManager only catches navigation changes and raises the LocationChanged event, the router registers with this event and does the actual work.

The key here is to get your head around what's actually going on, and what the various classes/objects are up to.

With this approach I no longer need to tamper with the back/forward history - the router handles "In Application" page changes and onbeforeunload warns the user if they are trying to navigate away.

Note that creating a customized version of the router isn't trivial. You will need to make copies of various other classes that are namespace restricted.

I'll put a post here when I have something polished enough to publish.

OlegQu commented 4 years ago

@ShaunCurtis thanks for reply! I already implemented steps 1 and 2. Should be enough to override OnLocationChanged event in Router class? And add there: if (hasChanges) _jsRuntime.InvokeAsync("onbeforeHandler"); And then just change all classes referenced with Router. Or this is not so simple? I have troubles with overriding Router, cause there are a lot of dependencies and idk how to register and use them after implementing. So I will look forward to see your approach (no matter how polished it is)! Thanks for your attention!

ArrowtheArcher commented 4 years ago

@ShaunCurtis struggling with step 4 cannot wrap my head around it is there any other way right now? is solution for this still in progress?

ShaunCurtis commented 4 years ago

I'm currently working on putting a working example together - literally as I write this. Hopefully I'll have something in the next day or two. This will include the "unwrapped" router code.

ShaunCurtis commented 4 years ago

I've now published the routing on GitHub with a working example project. You can install the DLL via a Nuget Package (I hope!). The source code is all there in the GitHub repository.

https://github.com/ShaunCurtis/CEC.Routing

ShaunCurtis commented 4 years ago

I've added a further project - CEC.FormControls - that uses the router with a more polished editor component similar to what I've been using in my projects. It uses enhanced Blazor InputBase Controls and the EditContext to manage editor control and routing. The Github repository is here:

https://github.com/ShaunCurtis/CEC.FormControls

dfkeenan commented 4 years ago

I just put CEC.Routing in my project. Works quite nicely. Thanks @ShaunCurtis .

nemtajo commented 4 years ago

Hi @ShaunCurtis. Thanks! Will this library work for web assembly project as well? I need to rewrite URL to redirect URL to always remove or always add a port. For example: localhost:5001/authorize to localhost/authorize. Is this possible with your library?

ShaunCurtis commented 4 years ago

Hi Nemtajo, While the functionality isn't built in, you can download the code from the repository and add it. In your case you will need to update the _locationabsolute private property in either the OnLocationChanged or Refresh Methods in the RecordRouter class. However, if you are changing the URL then you aren't routing but doing normal navigation???

nemtajo commented 4 years ago

I found a much simpler method, In Startup.cs of the server project I added:


            app.UseStaticFiles(new StaticFileOptions()
            {
                OnPrepareResponse = (context) =>
                {
                    var request = context.Context.Request;
                    var response = context.Context.Response;
                    UrlRewriteUtils.AddPortIfLocalHost(request, response);
                }
            });

    public class UrlRewriteUtils
    {
        public static void AddPortIfLocalHost(HttpRequest request, HttpResponse response)
       {
           string url = request.GetDisplayUrl();
            if (url.Contains("localhost/authorize"))
            {
                var newUrl = url.Replace("localhost/authorize", "localhost:5001/authorize");
                response.Redirect(newUrl.ToString(), true);
            }
        }
    }

I used this as a reference: //https://stackoverflow.com/questions/51105799/how-do-i-force-https-redirect-on-static-files-in-asp-net-core-2-1

ghost commented 4 years ago

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

smartprogrammer93 commented 3 years ago

I believe this issue should receive more love from the Asp.net team. It is vital in a lot of Enterprise application that you manage when a certain window is closing without saving or something like that (In windows forms that is). But in our company's case, we are using Blazor to build the UI of our enterprise application. Now if i can not manage the state of the page being navigated away from and stop it if the user clicked on cancel, the user might accidentally click on a link or something that will move him away from the page losing all the important input he already entered (That he is not able in his line of business to obtain more than once).

Thank you @ShaunCurtis for your implementation, I think i will use it to take ideas to build my own.

mrpmorris commented 3 years ago

See https://github.com/dotnet/aspnetcore/pull/24417

Although this ticket came first, perhaps it should be closed as a duplicate of #23886 because that's where the work has been done? @mkArtakMSFT

ShaunCurtis commented 3 years ago

Before this thread gets closed, I'd like to add a note. Since I did the routing work, my thinking and code have taken a rather more radical turn in solving the problem.

The root (pun intended) of the problem is the hoops we jump through to use URLs to navigate our way around an SPA application. So, I've thrown out Router entirely and NavigationManager gets ignored. They are replaced with ViewManager. You can read about my solution here in a CodeProject Article. The code is part of my CEC.Blazor project - Experimental Branch. You can see the code in action WASM and Server.

As I say it's radical, but it solves a great many problems routing and URL navigation throw up.

radderz commented 3 years ago

If a User clicks the back button on the browser on a path that shouldn't be allowed (where the back goes to a place within the same app so no full navigation) would this allow for some form of breaking that navigation?

The use case is when you have gone through a wizard, and once completed it's not ok to go back to the previous screen. You can handle this in the app but its a lot more code to handle all the instances of where I was and how I got to here type logic. If we could simple have a way to say use cannot go back or similar then you could block the user from trying to go to an unsupported location.

If this doesn't sound like a good way to handle this type of situation, how would you recommend handling it? Using a UI to show an error if the navigation history wasn't in the right order?

ShaunCurtis commented 3 years ago

As a further add on to my other musings on this subject, I've worked on a slightly less radical approach that dumping the router. Sometimes being a bit revolutionary helps you think a problem through more logically! There's an article here : A-Blazor-Modal-Dialog-Editor To summarise, normal routing, but all editing/user entry wizard stuff in a Modal Dialog that locks off the rest of the screen. you still need to turn on/off the browser are you sure, but that's a issue for all SPAs.

ghost commented 3 years ago

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. Because it's not immediately obvious that this is a bug in our framework, we would like to keep this around to collect more feedback, which can later help us determine the impact of it. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 3 years ago

Thanks for contacting us.

We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

GiffenGood commented 3 years ago

If I am reading this correctly, this issue is not going to addressed in .net 6?

smartprogrammer93 commented 3 years ago

@GiffenGood unfortunately, you read correctly.

It puzzles me though since this is one of the most requested festures in my opinion.

GiffenGood commented 3 years ago

@smartprogrammer93 This is just so disappointing. We have been waiting for this feature forever and it is just a fundamental feature for a spa. What user wants to use spa where an accidental click or keypress can result in loss of work?

smartprogrammer93 commented 3 years ago

@GiffenGood i mean there is a workaround, but you will have to copy the blazor router code from the repo and a bunch of other classes (since they sealed) just to create such a function.

oiBio commented 3 years ago

We were also waiting for this, to finally fix some user requests... Very disappointing:-(

@smartprogrammer93 do you have a link/repo to an working workaround?

Gaulomatic commented 3 years ago

@oiBio I went this route - pun intended. It broke with .NET 5 and I didn't bother to reimplement all those sealed classes, my time is too valuable. It was a very, very dirty hack involving the most insane use of reflection I ever wrote. So let us pray that anyone at Microsoft follows this issue and takes action. It still stuns me why they made it so hard to reimplement the router.

mrpmorris commented 3 years ago

I'm sure this is going to be quite tricky to do, but I think it is really important.

"Do you want to abandon your changes?" is a standard UI experience when abandoning unsaved changes.

Gaulomatic commented 3 years ago

I can' remember the exact details, it wasn't that tricky. I exposed an event taking a CancelEventArgs and was able to cancel the navigation elsewhere, if so desired. It worked well. But the code was beyond smelly because of those sealed classes. I believe something was pushed into the browser history if the navigation was canceled, but it this was better than loosing data.

smartprogrammer93 commented 3 years ago

I've added a further project - CEC.FormControls - that uses the router with a more polished editor component similar to what I've been using in my projects. It uses enhanced Blazor InputBase Controls and the EditContext to manage editor control and routing. The Github repository is here:

https://github.com/ShaunCurtis/CEC.FormControls

@oiBio you can check this reply for a repo. Idk if it works with .net5 but give it a try. Just know it is tricky to do, especially since his implentation removes the router altogether.

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

cederron commented 3 years ago

We really need this

Continuitis commented 3 years ago

+1 for the need. I think it's a basic for any app... Not having it is a blazor weakness...

ShaunCurtis commented 3 years ago

I've evolved the way I handle "Dirty" forms and dumped my earlier Router/Form Control solution. There are many ways in the browser to abandon a dirty form that a Blazor cancel navigation event doesn't cover. For those who want more information you can see my latest ideas here on my demo site - https://cec-blazor-database.azurewebsites.net/. There's also a short article on my personal Github site - https://shauncurtis.github.io/articles/Building-Edit-Forms.html.

So while I think the cancel option is still worthwhile, I'm no longer waiting on it to solve all my dirty form problems. Many of the problems are common across all SPA Frameworks.

CodeFontana commented 3 years ago

+1 for the need. We wouldn't be human if we've never accidentally navigated away from a page without saving our changes! Sometimes it's the little niceties that make all the difference :)

Thank you Microsoft, sincerely, Blazor is amazing.

andean00 commented 3 years ago

Hi, I just saw with horrow that this important feature got set aside and not going in. I have the same feeling like many people over here. I am commited with Blazor Wasm since the first release, and I have made magic to make it work with 2D and 3D stuff like threejs, pixijs and SVG. It has served me well on corporate applications/intranet environments.

People at MS: please make an extra effort and make this work soon. There are so many cases when people simply forget to save and just navigate away ! Thanks and be safe !

robertmclaws commented 3 years ago

I'm honestly surprised this feature isn't making the cut for 6.0. It's badly needed.

SL-AdamStevenson commented 2 years ago

Howdy,

Here is a simple solution that appears to work for .NET 5 - Blazor WebAssembly. Only steps required are modifying the service collection on startup, and implementing your own navigation manager that wraps the existing WebAssemblyNavigationManager. In the example linked below, the home page cancels any requests to the counter page.

https://github.com/SL-AdamStevenson/CancelableNavigationManager

Gig'em,

-Adam

FYI: @robertmclaws @SteveSandersonMS @captainsafia

robertmclaws commented 2 years ago

Hey @SL-AdamStevenson this looks awesome! How would you feel about contributing it to https://github.com/CloudNimble/BlazorEssentials? I would handle pulling it over, writing tests, and giving you credit on the check-in.

SL-AdamStevenson commented 2 years ago

Howdy @robertmclaws,

Not a problem; feel free to include it. Let me know if you need anything.

Gig'em!

ShaunCurtis commented 2 years ago

I've forked the project and made some changes to the code to deal with a navigation loop in certain circumstances. See the issue I raised in the project.

This solution only works in Web Assembly because Web Assembly implements loose coupling between the JS Navigation event and the NavigationManager. It doesn't use DI, but gets the only instance of WebAssemblyNavigationManager from the static object.

WebAssemblyNavigationManager.Instance.SetLocation(uri, isInterceptedLink);

In Server, it's more tightly coupled. The CircuitHost in the Blazor Hub gets the registered NavigationManager directly.

       var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
        navigationManager.NotifyLocationChanged(uri, intercepted);

I'm working on a work around solution that includes your CancelableNavigationManager. OK to use your code?

Could that be an aspnetcore project solution? Decouple the code in server so the community can provide solutions.

Liero commented 2 years ago

Couldn't this be handled in JavaScript?

  1. A custom Blazor Component would track EditForm's IsModified state in JavaScript object.
  2. There would be a JavaScript interrupting navigation. Either by listening on a suitable DOM event if there is any, or by overriding history.pushState?
ShaunCurtis commented 2 years ago

Blazor's JS code already does what you describe in 2. It's what triggers the routing event. Unless there's a way to selectively "disable" this then I don't believe it can be handled in Javascript. Can you put together some working code?

On Fri, 10 Dec 2021 at 16:43, Daniel Turan @.***> wrote:

Couldn't this be handled in JavaScript?

  1. A custom Blazor Component would track EditForm's IsModified state in JavaScript object.
  2. There would be a JavaScript interrupting navigation. Either by listening on a suitable DOM event if there is any, or by overriding history.pushState?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/aspnetcore/issues/14962#issuecomment-991125513, or unsubscribe https://github.com/notifications/unsubscribe-auth/AF6NT3LILOUIDXY7WCGE4U3UQIU4DANCNFSM4JAGNE4A .

-- Regards,

Shaun Curtis @.***

Liero commented 2 years ago

@ShaunCurtis:

given that shouldPreventNavigation() javascript functions returns true, when there are any unsaved changes, then following JavaScript prevents navigation when either user clicks <a> tag, or NavigationManager.NavigateTo(..):

window.history.pushState = (function (basePushState) {
    return function (...args): void {
        if (shouldPreventNavigation() && confirm('There are unsaved changes. Would you like to stay?')) {
            return;
        }
        basePushState.apply(this, args);
    }
})(window.history.pushState);

and following prevents refresh:

window.addEventListener('beforeunload', e => {
    if (shouldPreventNavigation()) {
        e.returnValue = 'There are unsaved changes'    
    }
});

Preventing back/forward is a little bit tricky, because:

  1. there is no way to prevent url change, we can only revert the change using history.back() resp history.forward()
  2. it's untrivial to detect whether user is navigating back or forward

    window.addEventListener('load', () => {
    let preventingPopState = false;
    function popStateListener(e: PopStateEvent) {
        if (shouldPreventNavigation()) {
            let shouldStay;
            //popstate can be triggered twice, but we want to show confirm dialog only once
            if (!preventingPopState) {
                preventingPopState = true;
                shouldStay = confirm('There are unsaved changes. Would you like to stay?');
            }
            if (preventingPopState || shouldStay) {
                //this will cancel Blazor navigation, but the url is already changed
                e.stopImmediatePropagation();
                e.preventDefault();
                e.returnValue = false;
            }
            if (shouldStay) {
                //detect back vs forward 
                const currentId = e.state && e.state.__incrementalId;
                const navigatingForward = currentId > lastHistoryItemId;
    
                //revert url
                if (navigatingForward) {
                    history.back();
                }
                else {
                    history.forward();
                }
                setTimeout(() => preventingPopState = false, 50); //avoid showing another confirm dialog when reverting
            }
        } 
    }
    window.addEventListener('popstate', popStateListener, { capture: true });
    
    let idCounter = 0;
    let lastHistoryItemId = 0; //needed to detect back/forward, in popstate event handler
    window.history.pushState = (function (basePushState) {
        return function (...args): void {
            if (shouldPreventNavigation() && confirm('There are unsaved changes. Would you like to stay?')) {
                return;
            }
            if (!args[0]) {
                args[0] = {};
            }
            lastHistoryItemId = history.state && history.state.__incrementalId;
            args[0].__incrementalId = ++idCounter; //track order of history items            
            basePushState.apply(this, args);
        }
    })(window.history.pushState);
    });