B4J Tutorial [ABMaterial] Avoid duplicate content when using dynamic controls

Hello all, I am going to try and explain how you can resolve the issue of dynamic content with ABMaterial. (This method works for me)

First lets start with the reason why we see duplicate content. What you created in BuildPage method is known as static content as it gets written in the html file which the browser caches it and what is created in ConnectPage is known as dynamic content as this get's written throught the websocket connection to the browser. The problem is when a user loads a page in the browser that has dynamic content, then looses connectivity and then regains connectivity a new server websocket class/page will be created at your server therefore a the Websocket_Connected event will raise again and will call the ConnectPage which appends the dynamic content over what the users has in the browser.
So lets take the following scenario:
you build an label in the build page with the text Static Content and you build another label in the ConnectPage with the text Dynamic Content. When a user open the page in the browser the server will server the html file which contains the label with Static Content, when the browser makes the websocket connection the server will serve the label with Dynamic Content. Now lets assume that the users looses connectivity but leaves the browser open (page loaded in which he sees both Static Content and Dynamic Content labels) now when he regains connectivity the browser will not Refresh(reload) the page instead will try to make a new websocket connection therefore the server will serve again the Dynamic Content label and the user will end up having 2 Dynamic Content Labels .... This is more or less how it works :)

Now how to avoid this:
1. in ABMShared at Class_Globals you need to have this:
B4X:
Public AppVersion As String = DateTime.now
2. in ABMShared at NavigateToPage you need to have this:
B4X:
Public Sub NavigateToPage(ws As WebSocket, PageId As String, TargetUrl As String)
    If AppVersion.Length > 0 Then TargetUrl = $"${TargetUrl}?${AppVersion}"$
    If PageId.Length > 0 Then ABM.RemoveMeFromCache(CachedPages, PageId)
    If ws.Open Then
        ' it doesn't keep navigation history in the browser (the back button exists the application)
        ws.Eval("window.location.replace(arguments[0])", Array As Object(TargetUrl)) 
        ' if you need browser history just comment the lines above and uncomment the lines below
'       ' it keeps the navigation history in the browser
'       ws.Eval("window.location = arguments[0]", Array As Object(TargetUrl))
        ws.Flush
    End If
End Sub
3. in ABMApplication (the entry point of the application) at WebSocket_Connected:
B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)
    Log("Connected")
    ws = WebSocket1
    ABMPageId = ABM.GetPageID(AppPage, ABMShared.AppName,ws)
    Dim session As HttpSession = ABM.GetSession(ws, ABMShared.SessionMaxInactiveIntervalSeconds) 
    session.SetAttribute("ValidSession", True)
    ... rest of your code ...
4. in every page/server websocket class at WebSocket_Connected:
B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)
    Log("Connected")       
    ws = WebSocket1
    ' here we check if the server has been restarted as the app version will have the date and time when the server started
    If ABMShared.AppVersion.Length > 0 And ws.UpgradeRequest.ParameterMap.ContainsKey(ABMShared.AppVersion) = False Then
        ABMShared.NavigateToPage(ws, "", "../")
    End If
    ABMPageId = ABM.GetPageID(page, Name,ws)
    Dim session As HttpSession = ABM.GetSession(ws, ABMShared.SessionMaxInactiveIntervalSeconds)
    ' if the session has expired the statement below will return false as there is no ValidSession attribute set for this session
    If session.GetAttribute2("ValidSession", False) = False Then
        ABMShared.NavigateToPage(ws, ABMPageId, "../")
    End If
    ... rest of your code ...


So let me explain a little point 1 sets the AppVersion as the time when the server was started and point 2 adds to NavigateToPage the AppVersion to the TargetUrl as a query string so we can have a reference in the browser when the server was started. Point 3 creates a session atrribute (ValidSession = True) as this is the entry point of the application and all clients must come through here - it will stay in session as long as the session is valid and as long as the session is valid the cache version of this page is valid too. Point 4 adds some checks in the WebSocket_Connected event that verify if the server has been restarted and if the current session is a valid session, if either one of them return false then the user will be redirected to the entry point of the application.

You kind of need to know your users connection or approximate ... If you are building an web app that will be mostly used on mobile phones there is a habbit that the user will not close the browser page and put the browser in minimized state - this kind of affects the heartbeat connection (to keep the session alive) even if the phone is connected to the internet, also if the phone looses internet connectivity for more time than you session timeout interval then the session will be destroyed as the heartbeat can't keep the session alive anymore. So it's your choice to handle the interval of how long a session will be kept and also how long the pages will stay in the cache why this setting from ABMShared Process_Globals:
B4X:
Public SessionMaxInactiveIntervalSeconds As Int = 30*60 ' 30 minutes '-1 = immortal but beware! This also means your cache is NEVER emptied!

So if you have a few users you could give this setting a higher value like 7 days (60*60*24*7) but if a lot of users access your app then you will need to keep this setting to a lower level as things will pile up in cache mechanism and also in the session store - and all this data is kept in your server (session and cache) ... so you are the only one that knows whats the perfect value for you.

I hope I haven't missed something :D and explained for everyone to understand ... if not fire your questions ;)
 

alwaysbusy

Expert
Licensed User
Longtime User
If ABMShared.AppVersion.Length > 0 And ws.UpgradeRequest.ParameterMap.ContainsKey(ABMShared.AppVersion) = False Then

Rereading your post, this line looks indeed as a good solution to find out if the server app was restarted, hence redirecting it to the entry point of the webapp. I'll build it in my personal apps too!
 

Harris

Expert
Licensed User
Longtime User
Rereading your post, this line looks indeed as a good solution to find out if the server app was restarted, hence redirecting it to the entry point of the webapp. I'll build it in my personal apps too!
When I tried @stanmiller solution, the app got into an endless loop - calling the entry point forever (my home page).

Hopefully this will work when -
Public NeedsAuthorization As Boolean = False '(or True)
As my site is both public and also needs login to access the members section.

When I have implemented this - shall let you know...
Thanks
 

stanmiller

Active Member
Licensed User
Longtime User
When I tried @stanmiller solution, the app got into an endless loop - calling the entry point forever (my home page).

I call that the spinning demon. :)

I use the same method as @mindful described with minor differences.
  • I don't have a separate check for when the server restarted or connection lost/timed out.
  • I store a date string in the session variable, where mindful uses a bool.
B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)

    ' Store particulars and get http session
    ws = WebSocket1
    ABMPageId = ABM.GetPageID(page, Name,ws)
    Dim session As HttpSession = ABM.GetSession(ws, ABMShared.SessionMaxInactiveIntervalSeconds)

   ' Get session id
   Private sSessionActive = session.GetAttribute2("SessionActive", SESSION_NONE ) As String
    Mtelog.Debug( "GetAttribute(SessionActive)=|" & sSessionActive & "|" )

   ...

   ' If no session then navigate home
   If ( sSessionActive = SESSION_NONE ) Then
          Mtelog.Debug( "Navigating home...")
          Mtelog.Debug("Home=" & TARGET_PAGE_HOME & "?loc=" & sLocCode )
          ABMShared.NavigateToPage(ws, ABMPageId, TARGET_PAGE_HOME & "?loc=" & sLocCode )
         Return
    End If

    ...

I have also implemented the concept of an "inside" and "outside" navigation. An outside navigation takes you to an external link. I found with Safari/iOS if the user navigates back from the external site, you get some kind of glitch (duplicate controls, spinning demon, I don't recall exactly which?)

1_webcalc_navin_out_zps7wuzso5e.jpg


Any external link is tagged with the NAV_OUT constant. Then in ParsEvent() the session variable is nulled as applicable.
B4X:
Private Sub Page_ParseEvent(Params As Map)
    Dim eventName As String = Params.Get("eventname")
    Dim eventParams() As String = Regex.Split(",",Params.Get("eventparams"))
    If eventName = "beforeunload" Then
        ABM.RemoveMeFromCache(ABMShared.CachedPages, ABMPageId)
        If ( nNavDirection = NAV_OUT And sClientBrowser.Contains("Safari") ) Then
            Mtelog.Debug("ABM: Preparing for url refresh. Setting session null.")
            ws.Session.SetAttribute( "SessionActive", SESSION_NONE )
        Else        
            Mtelog.Debug("ABM: Preparing for url refresh. Keeping session." )
        End If
        Return
    End If
    Mtelog.Debug( "<-----  EventName=" & eventName & ", paramsize=" & Params.Size )

SiteOne Calculator Web Edition
https://www.macthomasengineering.com/webcalc
 
Last edited:

Harris

Expert
Licensed User
Longtime User
One way or another, we shall overcome...

BTW, is your site.com built with ABM? It doesn't appear to be.

Thanks for your input and guidance. All of this helps.
 

stanmiller

Active Member
Licensed User
Longtime User
One way or another, we shall overcome...
BTW, is your site.com built with ABM? It doesn't appear to be.
Thanks for your input and guidance. All of this helps.

Cheers. I'm working on an ABM edition of macthomasengineering.com between projects. Today it's a single page index.php. SiteOne Landscaping's site was built by a third party unaffiliated with us.
 

Harris

Expert
Licensed User
Longtime User
Cheers. I'm working on an ABM edition of macthomasengineering.com between projects. Today it's a single page index.php. SiteOne Landscaping's site was built by a third party unaffiliated with us.
Look forward to your new ABM site (or pages)..

@alwaysbusy - Please setup a section where all us good folks can post a link to our projects built with ABM (ABM specific) - if even on your personal blog page....
Hopefully, this will inspire us all as to what we can create using this wonderful framework.

My view (and experience) is rather narrow - essentially based on what you have taught and demonstrate (copying code line by line... lazy or WHAT?).

I would dearly love to see others beautiful (ABM) creations (and code samples).

Thanks
 
Top