Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose programmatic access to the travellog #1093

Open
darbid opened this issue Mar 19, 2021 · 18 comments
Open

Expose programmatic access to the travellog #1093

darbid opened this issue Mar 19, 2021 · 18 comments
Assignees
Labels
feature request feature request tracked We are tracking this work internally.

Comments

@darbid
Copy link

darbid commented Mar 19, 2021

I am interested in creating the list of possible page titles/urls to navigate backwards and forwards. See the pic of the list from Edge. I am writing a WPF app.

Looking at the current API there is not much there. Am I right? There is a HistoryChanged event but only indicates the history has changed, but does not give you the actual history. I see a history count but not much else.

image

AB#32488716

@darbid
Copy link
Author

darbid commented Mar 21, 2021

As I could not find anything I played around with it. As in the picture I wanted to present to the user the available pages to GoBack or GoForward. Using Navigation and Frame Complete events I am pretty sure that I am catching the history. One complication is to work out where the user is in my generated history list. For example if the user just keeps clicking links then everything is OK as all the history is for the back button. However as soon as a user chooses to GoBack then you need to split your history between Back - Current - Forward.

For example if the user navigates from Bing, then to Github, then to Microsoft and then to Stack Overflow my history list would be;
0 - Bing - Back
1 - Github - Back
2 - Microsoft - Back
3 - Stack Overflow - Current

0,1,2 would all show on a list for GoBack. 3 is the current page and would not show on any list.

If the user clicks the GoBack button ONCE then the history list would look like this.
0 - Bing - Back
1- Github - Back
2 - Microsoft - Current
3 - Stack Overflow - Forward

0,1 would be on the GoBack list. 2 is the current page. 3 would now show on a forward list.

The Problem
Webview2 offers either a GoBack method
or a Navigate method.
Looking at an example. Imagine my list is as follows.
0 - Bing - Back
1- Github - Back
2 - Microsoft - Back
3 - Stack Overflow - Current
This means the user has navigated a few times from Bing through to Stack Overflow.
In my implementation I present the user with 0,1,2 in a list of possible GoBack items. If they choose "0 - Bing" then I use the Navigate method to navigate to this URL. The problem is that a Navigation "resets" the history.

The expectation after choosing "0 - Bing" would be
0 - Bing - Current
1- Github - Forward
2 - Microsoft - Forward
3 - Stack Overflow - Forward

But the result because I use the Navigate method is this;
0 - Bing - Current

WebView2 resets its History and now CanGoForward returns false.

The alternative would be to call GoBack multiple times. This would step WebView2 back in its history but would mean you have to navigate to all the pages in between.

I really hope I am missing something in my logic or in the API. If I am not, then giving users a history list to choose from will not work so well at least not as they would expect.

Edit: Of course one can use Javascript window.history.go(-2) etc.

@champnic
Copy link
Member

Hey @darbid - you are right, we don't currently have the API available that would make this straightforward, and the current workaround is to use the HTML5 History API + Navigation events to build the list. I don't think we've specifically gotten a request to expose the travellog in the past - do you want me to open this as a scenario on our backlog?

@champnic champnic self-assigned this Mar 24, 2021
@darbid
Copy link
Author

darbid commented Apr 13, 2021

@champnic my use case is to have a session history so as to mirror what normal browsers do when right clicking or long clicking on the back and forward buttons, however, I do recognize this is not a must have feature and def would have a very low priority for the development team.

At best this is a nice to have.

The issue I am seeing is being able to sync the history of the browser with my history. One example is This Android site. If you choose the menu items at the top there is no navigation (Start and Complete) but there are HistoryChanges and DocumentTitle changes. However, at the time of the History Change it is not clear what the Document Title is (it can be empty, old or something totally different). I am currently assuming that a document title change and History Change without a navigation starting is something I need to save for my session history.

Further the HistoryChange event exposes nothing. It is up to the developer to get the URL at that point in time. Given the async way WebView2 works it is possible (but I have not experienced it yet) that by the time the Source property is read that it is not the same as the URL which was the reason for the HistoryChange.

HTML5 History API
Please correct me if I am wrong but this API would only be available to us through Java script and as such the actual history of the user is not available.

Other APIs
Extensions have a History API which exposes a full collection of history items.
GeckFX exposed a full history collection of items which held the URL, Title and a boolean as to whether it was a subframe or not.

Suggestion on Developer Text would it be possible to Remark on why this event is fired. From what I can see if I make one navigation the HistoryChanged event fires twice. At the time of the first history change event the Source url has not changed. The second time it fires the Source url has changed. Is this event connected with the Window Popstate event or as I suspect is this event being raised from an internal Webview2 property with a property changing and property changed event?

Minimal Feature Suggestion
I would suggest you add the URL into the event args of the history change event.

@champnic champnic added feature request feature request tracked We are tracking this work internally. and removed question labels Apr 14, 2021
@champnic champnic changed the title Is the creation of the list of possible back or forward navigations up to the developer? Expose programmatic access to the travellog Apr 14, 2021
@champnic
Copy link
Member

I've added this as a scenario on our backlog.

Can you clarify this statement for me?

Please correct me if I am wrong but this API would only be available to us through Java script and as such the actual history of the user is not available.

What do you mean by "actual history of the user"? How does that differ from what the javascript API provides?

@darbid
Copy link
Author

darbid commented Apr 14, 2021

Can you clarify this statement for me?
Please correct me if I am wrong but this API would only be available to us through Java script and as such the actual history of the user is not available.

You referred to the HTML5 History API above. My understanding is that this is only good to us in Javascript to go back / forward (couple of other methods) and to get the length of the collection. There is no way to actually view the session history items with this API.

@champnic
Copy link
Member

Oh gotcha, yes. But by using the javascript API you avoid calling Navigate to go to a specific item and losing travellog state. It's just a workaround - WebView2 should still provide a better API to interact with the travellog.

@darbid
Copy link
Author

darbid commented Apr 15, 2021

To help anyone work on a SessionHistory / TravelLog here are a few things from my experience.

In my subclass of WebView2 I have 3 properties

private readonly HashSet<ulong> NavigationTracker = new();
private string PreviousURL;
private bool HistoryDidChange;

In my opinion you need to listen to NavigationStarting, NavigationCompleted, HistoryChanged and DocumentTitleChanged.

private void CoreWebView2_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
        {
         
            if (!e.IsRedirected && e.IsUserInitiated && NavigationTracker.Count != 0)
            {
                System.Diagnostics.Debug.Print("RESET NEEDED ************IsUserInitiated: " + e.IsUserInitiated.ToString() + "  IsRedirected: " + e.IsRedirected.ToString() + " *******");
            }
            else if (!e.IsRedirected)
            {
                NavigationTracker.Add(e.NavigationId);
            } else
            {
                System.Diagnostics.Debug.Print("NOT NEEDED ************IsUserInitiated: " + e.IsUserInitiated.ToString() + "  IsRedirected: " + e.IsRedirected.ToString() + " *******");
            }
        }

I have left part of my code which is still being tested to understand more fully what happens. The basic idea is that I add the NavigationID to the NavigationTracker.

private void CoreWebView2_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
        {
            NavigationTracker.Remove(e.NavigationId);

            if (HistoryDidChange)
            {
                HistoryDidChange = false;
                if (!string.Equals(this.CoreWebView2.DocumentTitle, this.CoreWebView2.Source) && NavigationTracker.Count == 0)
                {    //this is where I add to my travel log / Session History
                    string url = this.CoreWebView2.Source;
                    string host = this.Source.Host;
                    string title = this.CoreWebView2.DocumentTitle;
                }
            }
        }

Most important is to remove the NavigationID. Then if we have a History Change then we get some details and add to our travel log or SessionHistory.

private void CoreWebView2_HistoryChanged(object sender, object e)
        {
            if (!string.Equals(PreviousURL, this.CoreWebView2.Source))
            {
                PreviousURL = this.CoreWebView2.Source;
                //We have a new URL so add just the URL to your SessionHistory / TravelLog it is too early for Document Title or Favicons also if the URL is new then I set HistoryDidChange to true
            }
            //this is suggested in the documents
            this.BrowserCanGoBack = this.CoreWebView2.CanGoBack;
            this.BrowserCanGoForward = this.CoreWebView2.CanGoForward;
        }

HistoryChanged event seems to fire twice so I ignore a change if it was the previous URL. I learnt this from the C++ MS example.
The most important point here is that a new URL is added to our collection because the browser has added it to its stack.

private void CoreWebView2_DocumentTitleChanged(object sender, object e)
        {
            this.DocumentTitle = this.CoreWebView2.DocumentTitle;

            if (!string.Equals(this.CoreWebView2.Source,this.CoreWebView2.DocumentTitle) && NavigationTracker.Count == 0)
            {
                if (HistoryDidChange)
                {
                    HistoryDidChange = false;
                    //Start adding to the TravelLog / SessionHistory
                    string url = this.CoreWebView2.Source;
                    string host = this.Source.Host;
                    string title = this.CoreWebView2.DocumentTitle;
                }
            }
        }

I use the DocumentTitleChanged event because HistoryChanges take place without a navigation. As I want to save the documenttitle in my collection then this event seems perfect for me. Maybe source change would also work.

I hope this helps someone or if someone else has feedback on my experience that would be great.

One other thing is I do not use the WebView2 methods to go back or forward. I only use the Javascript method Window.History.Go(int); you add an int -1 to go back 1 or use a 2 to go forward 2 items in the history.

CoreWebView2.ExecuteScriptAsync(String.Format("window.history.go({0})", index.ToString()));

@darbid
Copy link
Author

darbid commented Sep 8, 2023

Wow, that was a while ago. You have to roll this yourself which seems a little frustrating as the browser holds the history. In the most simplest form you need to hold a list of urls that have been navigated, then each time a new url is added to that list you need to first check if the url already exists in the list. I did this before the new “Kind” was added, but (with the greatest respect) it is as good as tits on a bull.

This kind is at https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2navigationkind?view=webview2-dotnet-1.0.1938.49

“A navigation back or forward to a different entry in the session navigation history, like via CoreWebView2.Back(), location.back(), the end user pressing Alt+Left or other UX, or other mechanisms to navigate back or forward in the current session navigation history.”

So the navigation kind tells you if CoreWebView2.Back(), location.back() or the javascript version are used. YOU control those as a developer. You as a developer control how the user navigates (keyboard short cuts maybe harder but possible) the only thing you cannot control is what links the user clicks on in the webpage. Thus this new feature is of marginal help. Will explain below where it helps.

What would actually help you and I would be knowing the “session navigation history” which as can now be seen from their text above they know otherwise they would not be able to send the kind.

Any way back to your question.

So you have a list of the urls that are navigated. Each time there is a new navigation you add to the list, but you check if the url already exists.

If you are intending to show the user a list of possible back and forward urls as I have shown then you have to not only keep your list of urls but you need to order them and keep a record of your position in that list.

For example

  1. Always a new navigation CURRENT POSITION 5

    1. www.donald.com
    2. www.mickey.com
    3. www.snowwhite.com
      4.. www.goofy.com
    4. www.minnie.com. POSITION HERE

So your list has 5 urls. You hold your current position as 5 as that was the last navigation complete url.

  1. You supply the user on your UI with back and forward buttons. Easy part. As you know the user clicked back or forward you know what is going to happen, Webview2 will of course now confirm it. So imagine the user clicked the back button twice.

    1. www.donald.com
    2. www.mickey.com
    3. www.snowwhite.com POSITION HERE
      4.. www.goofy.com
    4. www.minnie.com.

So your list does not change but you keep a record that your position is at 3.

  1. Now imagine on the www.minnie.com page there is a link to www.snowwhite.com when navigation compete is completed for www.snowwhite.com you would add it to your list. Here you would find out it already exists but the KIND would NOT fire for this which means you would add www.snowwhite.com. Below is how it would now look.

    1. www.donald.com
    2. www.mickey.com
    3. www.snowwhite.com
      4.. www.goofy.com
    4. www.minnie.com.
    5. www.snowwhite.com POSITION HERE
  2. Finally if you supply a UI list to the user as Webbrowsers do (a long mouse button press on the back and forward arrows) and as can be seen in my screen shoot above then the user can choose a position in the backwards or forwards history. Let’s imagine the user chooses www.mickey.com. so you cannot call navigate otherwise your travel log will be out of step with webview2. You need to go back 4 times. I did this with javascript and Back() method where you can put a -2 for example. You of course have to update your list to show the current position.

    1. www.donald.com
    2. www.mickey.com POSITION HERE
    3. www.snowwhite.com
      4.. www.goofy.com
    4. www.minnie.com.
    5. www.snowwhite.com

If you do my No.4 example this way then your back and forward method attached to Webview2 still work and are in sync with your list.

@ClosetBugSlayer
Copy link

Hey @darbid - you are right, we don't currently have the API available that would make this straightforward, and the current workaround is to use the HTML5 History API + Navigation events to build the list. I don't think we've specifically gotten a request to expose the travellog in the past - do you want me to open this as a scenario on our backlog?

The above method of manually tracking the browsing history is tedious and cannot be persisted to new process instances. So yes, open this new scenario if you haven't already.

We need a way to persist and restore the travellog (for reference, we did this with ITravelLogStg and ITravelLogEntry in Internet Explorer). I would prefer it be done independently of the WebView2 user data folder so that we can map it to our specific UI views. Ideally each entry would have title, URI, and icon URI. Any form data is a bonus.

I don't know if this is related but browsers now allow you to middle-click the forward/back buttons to do a combination of new window, navigation, and history propagation. I think GoBack() and GoForward() should have two additional parameters: number of positions to navigate from the current position (like the Javascript API), and whether or not to open as a new window.

@FrayxRulez
Copy link

FrayxRulez commented Jul 18, 2024

As a workaround you can use CallDevToolsProtocolMethodAsync("Page.getNavigationHistory", "{}").

@MarkIngramUK
Copy link

Having a host-application navigation stack is not a viable work around for the lack of access to WebView2's navigation stack. If we maintain our own navigation stack, but WebView2 crashes, and then reloads, WebView2 will lose it's internal navigation stack. That means that the following events (that are outside the control of the host-application) will not succeed:

  • Keyboard shortcuts (Alt+Left arrow / Alt+Right arrow)
  • Context menu items Back / Forward
  • Any web page that uses JavaScript to control the navigation stack, e.g. history.back() / history.forward()

To fix these use-cases, we need to be able to get & set the navigation stack (or have WebView2 automatically restore it's stack after a crash).

CC @nishitha-burman

@Slion
Copy link

Slion commented Sep 1, 2024

Related to #4713

It's not just travel logs, you also want to be able to restore the position on each page part of your history.
That's so easily done on Android, hard to believe no one bothered prioritising this feature over the years. Persistence is a very common software feature.

@Slion
Copy link

Slion commented Sep 12, 2024

Even in Edge page scroll offset is not persisted only the travel logs.
Though until you restart your session scroll offset is kept as expected as you go back or forward in your travel logs.

@aluhrs13
Copy link
Contributor

aluhrs13 commented Oct 8, 2024

Having a host-application navigation stack is not a viable work around for the lack of access to WebView2's navigation stack. If we maintain our own navigation stack, but WebView2 crashes, and then reloads, WebView2 will lose it's internal navigation stack. That means that the following events (that are outside the control of the host-application) will not succeed...

  • Keyboard shortcuts (Alt+Left arrow / Alt+Right arrow)
  • Context menu items Back / Forward
  • Any web page that uses JavaScript to control the navigation stack, e.g. history.back() / history.forward()

@MarkIngramUK - Agree that it's less than ideal, but this should be tractable with a few changes (making some assumptions). Definitely expand on the issue if I'm missing something major here, that might help with prioritization:

  1. Maintain a list of history in your host app using CoreWebView2NavigationStarting (and/or CoreWebView2NavigationCompleted) in a way that can persist across restarts.
  2. Implement your own back and forward functions that navigates based on that history. Use CoreWebView2CanGoBack / CoreWebView2CanGoForward to understand if you can just use WV2's back and forward APIs. If not, CoreWebView2.Navigate to the URL you have saved on the native side.
  3. Change keyboard shortcuts to call your new functions.
  4. Use CoreWebView2.ContextMenu* APIs to remove the default back/forward buttons and add your own that call to your function.
  5. Use CoreWebView2.AddScriptToExecuteOnDocumentCreated to inject a script that listens for the popstate event and calls out to your native code to navigate back/foward when it's fired. EDIT: popstate is probably not the right event, something from the Navigation API like currententrychange is probably better.

I'm sure there's implementation nuance that I'm overlooking around handling "your own" navigations in #5, but (naively obvious) doesn't seem prohibitively expensive?

It does feel like there's a gap in history's popstate only firing for some changes to history... Might be more keen on pursuing getting a web standard there if that ends up being a/the major issue. EDIT: The events from the Navigation API spec handle this better than the History API.

@ClosetBugSlayer
Copy link

Having a host-application navigation stack is not a viable work around for the lack of access to WebView2's navigation stack. If we maintain our own navigation stack, but WebView2 crashes, and then reloads, WebView2 will lose it's internal navigation stack. That means that the following events (that are outside the control of the host-application) will not succeed:

  • Keyboard shortcuts (Alt+Left arrow / Alt+Right arrow)
  • Context menu items Back / Forward
  • Any web page that uses JavaScript to control the navigation stack, e.g. history.back() / history.forward()

To fix these use-cases, we need to be able to get & set the navigation stack (or have WebView2 automatically restore it's stack after a crash).

There is other state connected to travel log that is less obvious, like page position, form fields, etc. What we really need is something more like an abstract BLOB that we can import and export which contains all of these things. It isn't unreasonable to use a web browser control for actual web browsing; right now the only use case seems to be for creating HTML dialog boxes.

@MarkIngramUK
Copy link

@aluhrs13 , thanks for the response. It’s possible we could invest in that work, and end up with a half-decent work around, but it’s a heck of a lot more effort than our macOS Browser that’s based on WebKit. Apple provides APIs for this scenario:

WKWebView.interactionState
WKWebView.backForwardList

WKWebView.interactionState returns binary data that can be later used to reinstantiate a WKWebView object (that data includes the navigation stack). We use it for session restoration. WKBackForwardList lives beyond the rendering process, so crashes in child processes don’t invalidate the stack.

@aluhrs13
Copy link
Contributor

aluhrs13 commented Oct 9, 2024

@MarkIngramUK - Yeah, I'm familiar with the similar WKWebView and Android WebView APIs. I don't have a ton of first-hand experience though, and Apple's docs are kinda lacking and I'm not finding much with quick search - Do you know what state is actually restored with interactionState?

Transparently - An implementation existing for other WebViews doesn't greatly affect our prioritization here. Given the main use-case requires:

  1. App that is a browser or browser-like scenario.
  2. WV2 Crashes
  3. User wants/needs to engage with back/forward

The combination is niche enough that for the apps that need it, the work-around isn't problematic enough for us to make this a higher priority.

@ClosetBugSlayer
Copy link

ClosetBugSlayer commented Oct 9, 2024

Transparently - An implementation existing for other WebViews doesn't greatly affect our prioritization here. Given the main use-case requires:

  1. App that is a browser or browser-like scenario.
  2. WV2 Crashes
  3. User wants/needs to engage with back/forward

The combination is niche enough that for the apps that need it, the work-around isn't problematic enough for us to make this a higher priority.

I don't know anything about WebKit or how it handles its state yet the earlier description reads pretty close to what I proposed. I'm trying to lay out the right thing to do for any embedded browser from a bird's eye POV.

What if the WV2 doesn't crash but the application simply has to be resumed after updates or end-of-work-day? Not to mention how much this happens during development or diagnosis. Would you say that's niche?

What if a host window is displaying 5 or 6 WV2's for real time display ex. financial or logistics or security? This is actually closer to what I'm using WV2 for, watching video feeds from multiple different sources. In that case you either have to maintain all your state in the URL or hope the cookies can figure out which view is which. If we don't control the source website in each WV2 then things get that much harder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request feature request tracked We are tracking this work internally.
Projects
None yet
Development

No branches or pull requests

7 participants