B4J Tutorial [ABMaterial] 1.09 all about flexibility

This is the first of a two part tutorial on some big changes I'm making to ABMaterial in 1.09. It should be fully backwards compatible so no (or very, very little) changes will be needed from your side on your existing projects if you stay working the pre-1.09 way.

I've rewritten the demo app in a dynamic way using the 'static' version and it went swift. And it creates a lot of new potential!

PART 1: Flexibility
-----------------
When ABMaterial 0.x first came out, it was especially useful for static pages. Since then, I gradually made more and more components dynamic. In 1.09, every component gets the upgrade (even ModalSheets!).

In the next download you will find a completely 'rewritten' Demo app being 100% dynamic.

So the steps I had to do to make a Page dynamic:
For the demo app, first I did this in ABMShared for preparation (the steps look very intensive, but I've tried to make it as understandable as possible):

1. Create a new method: ConnectNavigationBar()
2. Move all lines where you build the topbar and sidebar to ConnectPage()

NOTES:
a. In BuildNavigationBar, we have to add a DUMMY item for the top and sidebar:
B4X:
 ' you must add at least ONE dummy item if you want to add items to the topbar   in ConnectNaviagationBar
   page.NavigationBar.AddTopItem("DUMMY", "DUMMY", "", "", False)

   ' you must add at least ONE dummy item if you want to add items to the sidebar
   page.NavigationBar.AddSideBarItem("DUMMY", "DUMMY", "", "")
b. In ConnectNavigationBar, we have to remove those DUMMIES
B4X:
  ' Clear the dummies we created in BuildNavigationBar
    page.NavigationBar.Clear

Full code:
B4X:
Sub BuildNavigationBar(page As ABMPage, Title As String, logo As String, ActiveTopReturnName As String, ActiveSideReturnName As String, ActiveSideSubReturnName As String)
   page.SetFontStack("arial,sans-serif")

   ' we have to make an ABMImage from our logo url
    Dim sbtopimg As ABMImage
    sbtopimg.Initialize(page, "sbtopimg", logo, 1)
    sbtopimg.SetFixedSize(236, 49)

   page.NavigationBar.Initialize(page, "nav1", ABM.SIDEBAR_MANUAL_HIDEMEDIUMSMALL, Title, True, True, 330, 48, sbtopimg, ABM.COLLAPSE_ACCORDION, "nav1theme") 
   page.NavigationBar.TopBarDropDownConstrainWidth = False
   page.NavigationBar.ActiveTopReturnName = ActiveTopReturnName
   page.NavigationBar.ActiveSideReturnName = ActiveSideReturnName
   page.NavigationBar.ActiveSideSubReturnName = ActiveSideSubReturnName
 
   ' you must add at least ONE dummy item if you want to add items to the topbar   in ConnectNaviagationBar
   page.NavigationBar.AddTopItem("DUMMY", "DUMMY", "", "", False)
 
   ' you must add at least ONE dummy item if you want to add items to the sidebar 
   page.NavigationBar.AddSideBarItem("DUMMY", "DUMMY", "", "")
End Sub

Sub ConnectNavigationBar(page As ABMPage)
    ' Clear the dummies we created in BuildNavigationBar
    page.NavigationBar.Clear
 
    ' new behaviour: on each top item you can set if it should hide of not on a medium or small device.
    page.NavigationBar.AddTopItem("Contact", "", "mdi-action-account-circle", "", False)

    ' shortened for this tutorial, you move them all!
    ' --------------------------------------------
    page.NavigationBar.AddSideBarItem("Icons", "Icons", "mdi-action-account-circle", "../IconsPage/abmaterial-material-icons.html")  
    '... the rest
End Sub
3. Create a new Method: ConnectFooter() and ConnectFooterFixed()
4. Move all lines where you add components to ConnectFooter() & ConnectFooterFixed()
B4X:
Sub BuildFooter(page As ABMPage)     
    page.Footer.AddRows(1, True, "").AddCellsOS(2,0,0,0,6,6,6, "")
    page.Footer.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components 
 
    page.Footer.UseTheme("footertheme")     
End Sub

Sub ConnectFooter(page As ABMPage) 
    Dim lbl1 As ABMLabel
    lbl1.Initialize(page, "footlbl1", "Blog: Alwaysbusy's Corner{BR}{BR}B4J by Anywhere Software{BR}Materialize CSS by students from Carnegie Mellon University",ABM.SIZE_PARAGRAPH, False, "whitefc")
    page.Footer.Cell(1,1).AddComponent(lbl1)
 
    Dim lbl2 As ABMLabel
    lbl2.Initialize(page, "footlbl2", "ABMaterial Copyright @2015-2016{BR}By Alain Bailleul{BR}{BR}Email: alain.bailleul@telenet.be",ABM.SIZE_PARAGRAPH, False, "whitefc")
    page.Footer.Cell(1,2).AddComponent(lbl2) 
End Sub

Sub BuildFooterFixed(page As ABMPage) 
    page.isFixedFooter= True
    ' because we have a fixed footer at the bottom, we have to adjust the padding of the body in pixels
    page.PaddingBottom = 200
 
    page.Footer.AddRows(1, True, "").AddCellsOS(2,0,0,0,6,6,6, "")
    page.Footer.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components 
 
    page.Footer.UseTheme("footertheme") 
End Sub

Sub ConnectFooterFixed(page As ABMPage)     
    Dim lbl1 As ABMLabel
    lbl1.Initialize(page, "footlbl1", "Blog: Alwaysbusy's Corner{BR}{BR}B4J by Anywhere Software{BR}Materialize CSS by students from Carnegie Mellon University",ABM.SIZE_PARAGRAPH, False, "whitefc")
    page.Footer.Cell(1,1).AddComponent(lbl1)
 
    Dim lbl2 As ABMLabel
    lbl2.Initialize(page, "footlbl2", "ABMaterial Copyright @2015-2016{BR}By Alain Bailleul{BR}{BR}Email: alain.bailleul@telenet.be",ABM.SIZE_PARAGRAPH, False, "whitefc")
    page.Footer.Cell(1,2).AddComponent(lbl2) 
End Sub

Ok preparations are done, let's move to an example of making a Page dynamic now:
1. Create a new method: ConnectPage()
2. Move all lines where you add components, modal sheets etc to ConnectPage()
3. Add the following line to the BuildPage() method (I'll explain further why):
B4X:
page.ShowLoaderType=ABM.LOADER_TYPE_MANUAL ' NEW
4. Add the following line to the start of ConnectPage()
B4X:
    'NEW
    ABMShared.ConnectNavigationBar(page)
5. Add the following lines to the end of ConnectPage()
B4X:
' also add the components to the footer
    ABMShared.ConnectFooter(page)
 
    page.Refresh ' IMPORTANT
 
    ' NEW, because we use ShowLoaderType=ABM.LOADER_TYPE_MANUAL
    page.FinishedLoading 'IMPORTANT

Full code:
B4X:
public Sub BuildPage()
    ' initialize the theme
    BuildTheme
 
    ' initialize this page using our theme
    page.InitializeWithTheme(Name, "/ws/" & AppName & "/" & Name, False, theme)
    page.ShowLoader=True
    page.ShowLoaderType=ABM.LOADER_TYPE_MANUAL ' NEW
    page.PageTitle = "ABMCodeLabel"
    page.PageDescription = "The code label component " 
    page.PageHTMLName = "abmaterial-code-label.html"
    page.PageKeywords = "ABMaterial, material design, B4X, B4J, SEO, framework, search engine optimization"
    page.PageSiteMapPriority = "0.50"
    page.PageSiteMapFrequency = ABM.SITEMAP_FREQ_MONTHLY
    page.UseGoogleAnalytics(ABMShared.TrackingID, Null) ' IMPORTANT Change this to your own TrackingID !!!!!!!
     
    ABMShared.BuildNavigationBar(page, "ABMCodeLabel", "../images/logo.png", "", "Controls", "ABMCodeLabel")
     
    ' create the page grid
    page.AddRows(5,True, "").AddCells12(1,"")
    page.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
     
    ABMShared.BuildFooter(page)
End Sub

Sub ConnectPage()
    'NEW
    ABMShared.ConnectNavigationBar(page)
    ' add paragraph 
    page.Cell(1,1).AddComponent(ABMShared.BuildParagraph(page,"par1","ABMCodeLabels are blocks that represent source programming code within your page.  It can be used, like for this app, to make a help website or tutorial that explains your library.") ) 
    ' add paragraph 
    page.Cell(1,1).AddComponent(ABMShared.BuildParagraph(page,"par2","There are two methods to setup your source code string: Using a B4J List object or using the B4J Smart Strings.  An example of both, you can use the one you feel most comfortable with.  Note, Smart Strings cannon show everything: Using as code Sub Name() ... End Sub ... Sub Name2() ... End Sub will not work with Smart Strings!") ) 
 
    ' add codeblock 
    Dim code1 As StringBuilder
    code1.Initialize
    code1.Append("// add codeblock").Append(CRLF) 
    code1.Append("Dim code2 As StringBuilder").Append(CRLF)
    code1.Append("code2.Initialize").Append(CRLF)
    code1.Append("code2.Append(""Dim btn4 As ABMButton"").Append(CRLF)").Append(CRLF)
    code1.Append("code2.Append(""btn4.Initializefloating(page, ""btn4"", ""mdi-image-palette"", ""darkred"")"").Append(CRLF)").Append(CRLF)
    code1.Append("code2.Append(""btn4.Large = True"").Append(CRLF)").Append(CRLF)
    code1.Append("code2.Append(""page.Cell(5,1).AddComponent(btn4)"").Append(CRLF)").Append(CRLF)
    code1.Append("page.Cell(6,1).AddComponent(ABMShared.BuildCodeBlock(page, ""code2"", code2))").Append(CRLF)
     
    page.Cell(2,1).AddComponent(ABMShared.BuildCodeBlock(page, "code1", code1))
 
    ' add codeblock 
    Dim code4 As StringBuilder
    code4.Initialize
    code4.Append("// add codeblock").Append(CRLF) 
    code4.Append("Dim code3 As String = $""Dim btn4 As ABMButton").Append(CRLF)
    code4.Append("btn4.Initializefloating(page, ""btn4"", ""mdi-image-palette"", ""darkred"")").Append(CRLF)
    code4.Append("btn4.Large = True").Append(CRLF)
    code4.Append("page.Cell(5,1).AddComponent(btn4)""$").Append(CRLF)
    code4.Append("page.Cell(6,1).AddComponent(ABMShared.BuildCodeBlockFromSmartString(page, ""code3"", code3))").Append(CRLF)
         
    page.Cell(2,1).AddComponent(ABMShared.BuildCodeBlockFromSmartString(page, "code4", code4))
 
    ' also add the components to the footer
    ABMShared.ConnectFooter(page)
 
    page.Refresh ' IMPORTANT
 
    ' NEW, because we use ShowLoaderType=ABM.LOADER_TYPE_MANUAL
    page.FinishedLoading 'IMPORTANT
End Sub
6. In Page_ready() call our ConnectPage() method:
B4X:
Sub Page_Ready()
    Log("ready!")
 
    ' NEW
    ConnectPage
 
    page.RestoreNavigationBarPosition
End Sub

Done! We made our page dynamic.

Some explanation what is happening:

In a 'static' page, all the components are written in the .html file. In a 'dynamic' page they are not. Only when you connect as a user, the components are inserted in their browser.

This seems trivial, but this actually means we can 'write' components (html, css and javascript) at run-time, depending on e.g. the user that logged in. Some user is allowed to see one chart, another user another chart. Or one may be able to delete a record from your database, another is not. Or showing a certain modal sheet depending on the user without having to add all possibilities in the HTML. Or, like will be explained in tutorial part 2, show the page in different languages depending on who logged in!

Why did we need to add those extra lines in BuildPage() and ConnectPage()?
page.ShowLoaderType=ABM.LOADER_TYPE_MANUAL means: We are going to take control when the 'running circles' should hide. As the page is loaded dynamical, without this line the user actually sees the page being build. This can be confusing (e.g. the user sees a button, wants to click it, but suddenly a chart pops up).

So now we have to tell manualy when everything is in its place, using page.FinishedLoading in ConnectPage().

Note that we also here run our FooterConnect() and a page.refresh()! This MUST be done, as it gives the action from the server to the browser to actually perform your changes.

How come we don't have to add those page.Needs...() properties for the components we'll add outside BuildPage()?
When you run (build) you app, ABMaterial wil inspect the B4J source code and try to find out what components you'll planning to use. It generates a text file next to your .jar called: copymewithjar.needs. As the name suggests, when you 'install' your app to your real server, you MUST copy this file next to your .jar file. Why? Because at that time, the B4J source code is no longer available for expection so it uses this file to fill in the page.Needs...() when you start the .jar.

This is an example of a generated copymewithjar.needs file for one of my apps:
B4X:
ABMApplication:NeedsInput;NeedsTextArea;NeedsMask
AgendaPage:NeedsCalendar;NeedsSwitch
ProspectPage:NeedsRadio;NeedsInput;NeedsTextArea;NeedsMask;NeedsCombo;NeedsSlider;NeedsGoogleMap;NeedsLists;NeedsDateTimeScroller;NeedsSwitch
ReferentiesPage:NeedsRadio;NeedsInput;NeedsTextArea;NeedsMask;NeedsCombo;NeedsSlider;NeedsGoogleMap;NeedsLists
StartPage:NeedsInput;NeedsTextArea;NeedsMask;NeedsPagination;NeedsTable;NeedsSortingTable;NeedsTabs;NeedsCombo;NeedsDateTimeScroller;NeedsSwitch

I've tried to do this analysis to a certain extend. (e.g. it can 'see' if you are needing a component you declared in a module and are using in your page class). But, if somehow it was unable to find it because your code structure is very complex and jumps all around, you will still need to set the page.Needs...() property yourself in BuildPage(). But this shouldn't happen very often. e.g. while converting all my apps, I never had to do it once.

Still a lot of testing to do, but it's looking good ;)

This concludes Part 1. In Part 2 I'll show you how we can use localization in ABMaterial.

Alain
 
Last edited:

Eric Baker

Member
Licensed User
Longtime User
Wow, I am very excited to read this. Still just getting started (part time) with the framework and it looks extremely promising. I haven't given it much thought that the pages up to now were static. This may or may not have caused some headache in the future and these changes drive me even more to continue to explore and support ABMaterial.
 

Roberto P.

Well-Known Member
Licensed User
Longtime User
Excellent improves!
It would be interesting also enter Ajax to optimize the loading and updating of a part of the page.
I hope not too much to ask.
Thank you
 

alwaysbusy

Expert
Licensed User
Longtime User
ABMaterial uses some ajax, but only where applicable. (e.g. to upload a file). ABMaterial is using websockets, which is the modern way browsers handle communication. Ajax is the old way of doing things. ABMaterial is already fully optimized to only update parts of the page. This is why every component has its own refresh. If you use a components refresh, nothing else in the page is touched. Of course if you always use page.refresh then the whole page is refreshed. It's up to you as a programmer to minimize the number of component refreshes needed.
 

imbault

Well-Known Member
Licensed User
Longtime User
Very great, Alain, when should we expect that V1.09?
Thanks for all your work
 

Harris

Expert
Licensed User
Longtime User
I don't know what to think... yet.
I shall have to see how it affects what I have already built.

Typically, what you do is all good for us. I just have to apply, and understand it.
I am trying to develop a webapp for a client where new ABM updates don't affect my current implementation.

Thanks
 

Harris

Expert
Licensed User
Longtime User
Sub Page_NavigationbarClicked(Action As String, Value As String)
page.ws.Session.SetAttribute("UserAction", Action)

Sub AddCaseBtn_Clicked(Target As String)
If page.ws.Session.GetAttribute2( "UserAction", "") = "Company" Then

With 1.10, do I still need to use above to prevent each user from stomping what other users have selected (or are viewing)?

Thanks
 

Harris

Expert
Licensed User
Longtime User
In 1.10

I have noticed that the page.Refresh may want to be inserted in the Page.Ready(), rather than ConnectPage(), since other methods may be called after this (your simple example).

B4X:
Sub Page_Ready()
    Log("Employee ready!")
    Page_name = page.ws.Session.GetAttribute2( "UserAction", "")  ' needed if connectpage() calls methods that must know Case and IF statements directives
   
    ConnectPage
    page.RestoreNavigationBarPosition
    page.NavigationBar.Title = page.ws.Session.GetAttribute2( "UserAction", "")  ' set the current page title ( page.PageTitle - doesn't work here...)
  

' called after ConnectPage()
    LoadCases(1)  ' without page refresh below, this grid won't be shown
   
    page.Refresh
       
    page.Footer.Cell(2,1).Refresh
   
    ' if visibility was to hide( by default ) , then unhide them
   
    If (Page_name = "Vehicles")  Then
       page.Row(1).Refresh   
    End If
    If (Page_name = "Zones")  Then
       page.Row(2).Refresh   
    End If
    If (Page_name = "Employees")  Then
       page.Row(3).Refresh   
    End If
   
End Sub

Updating one's many pages to 1.10 dynamic is rather trivial... when you figure out what goes where.
With my "One Page, Many Uses" strategy, and setting a page.session "UserAction" attribute, all functions well.

Good on ya!
 
Top