B4J Tutorial Android2ABMaterial: A Personal Experiment

Hi

Well, as a newbie to the ABMaterial framework, about a month and a couple of days now, I decided to take a journey to see if I can create a webapp using the ABMaterial framework. This app would be based on my Android App. The learning curve was steep but after I had figured out some things as I went on through the many questions I asked with positive feedback from the forum, this is just to share what I have learned.

I'm sure if you have a better way, you will advise for the betterment of the knowledge about the use of the framework.

Please note: The demo app that comes with ABMaterial has a wealth of information, I found myself having to refer and re-refer to it a lot of times to get what was happening. This experiment is based on ABMaterial 1.22.

Please also see my blogs on the ABMaterial Themes Quick Reference and also My take on the Grid
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Sign In

The purpose of the sign in screen is to provide access to the app for users. A user signs into the app using their email address and their password. In this case, the app has been set to use the mobile phonenumber as a password. It's not safe, but its the requirement from the specs.

From the sign in page, one can navigate to the Sign Up page, and if they forgot their passwords, enter their email address and the password will be emailed to them. To send emails, I'm using smtp and its working so far. As I am not very clued up on the websockets and what they do, I believe as soon as I'm clear with that world, I will have a better playground. As the app will also work on small devices, my plan was for the ActionButton to show only on small devices, so with that I used the visibility property.

To be able to sign in, the email and password are required, thus a prompt will show if any of these is not entered. I use a map to store the contents of the form and these get verified against the database. To make my approach work i had to add some methods to ABMShared and also based of my thinking patterns use the same approach that DBUtils followed. It's not a major change and it does the same thing.

SignIn.png


This is running inside a Ripple emulator inside Google Chrome. This experiment is based on ABMaterial 1.22.

Theming the app: To have a theme as per requirements, I used a navigator theme and some input themes to make the Orange look. I also use toast themes, red when there is an error and green when something went well.

B4X:
MyTheme.AddToastTheme("toastred")
    MyTheme.Toast("toastred").backcolor = ABM.COLOR_RED
    MyTheme.Toast("toastred").actionforecolor = ABM.COLOR_BLACK
    MyTheme.AddToastTheme("toastgreen")
    MyTheme.Toast("toastgreen").backcolor = ABM.COLOR_GREEN
    MyTheme.Toast("toastgreen").actionforecolor = ABM.COLOR_BLACK

I wanted my page background and sidebar navigation bar to follow the same look and feel, so I used this theme.

B4X:
MyTheme.AddNavigationBarTheme("nav1theme")
    MyTheme.NavigationBar("nav1theme").sidebarbackcolorintensity = ABM.INTENSITY_LIGHTEN5
    MyTheme.NavigationBar("nav1theme").sidebarbackcolor = ABM.COLOR_GREY
    MyTheme.NavigationBar("nav1theme").topbaractiveforecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.NavigationBar("nav1theme").topbarfontsize = 7
    MyTheme.NavigationBar("nav1theme").topbaractiveforecolor = ABM.COLOR_ORANGE
    MyTheme.NavigationBar("nav1theme").sidebarforecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.NavigationBar("nav1theme").topbarforecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.NavigationBar("nav1theme").sidebarforecolor = ABM.COLOR_ORANGE
    MyTheme.NavigationBar("nav1theme").topbarforecolor = ABM.COLOR_ORANGE

As I will also have ABMLists in my app, which will be both dynamic and static, I created a theme for these to fit into the page backgrounds..

B4X:
MyTheme.AddListTheme("greylist")
    MyTheme.List("greylist").itembackcolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").subitembackcolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").backcolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").itembackcolor = ABM.COLOR_GREY
    MyTheme.List("greylist").subitembackcolor = ABM.COLOR_GREY
    MyTheme.List("greylist").subitemhovercolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").itemactivecolor = ABM.COLOR_GREY
    MyTheme.List("greylist").backcolor = ABM.COLOR_GREY
    MyTheme.List("greylist").itemactivecolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").itemhovercolorintensity = ABM.INTENSITY_LIGHTEN4

And finaly for ABMInput, used...

B4X:
MyTheme.AddInputTheme("inputtheme")
    MyTheme.Input("inputtheme").inputcolor = ABM.COLOR_ORANGE
    MyTheme.Input("inputtheme").forecolor = ABM.COLOR_ORANGE
    MyTheme.Input("inputtheme").forecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.Input("inputtheme").inputcolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.Input("inputtheme").focusforecolor = ABM.COLOR_ORANGE
    MyTheme.Input("inputtheme").focusforecolorintensity = ABM.INTENSITY_LIGHTEN1
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Sign In: Continued

The TopButtons in the navigation bar will follow the same functionality with the ABMActionButton SubAction buttons. They are Sign In, Sign Up, Forgot Password and Go Back

Sign In: Check if email and password are entered and process sign in
Sign Up: Go to the Sign Up page
Forgot Password: Check if email is entered and if so, read the database and send email to user with the password.

Some of my pages need to be authorised and others e.g. welcome page, contact us, sign in do not. To enable that I saved the authorization status of each page so that when each page is accessed, the session is checked and if not authorised, so back to the welcome page. So in ABMApplication, Websocket_Connected method I added this script just after ConnectPage call.

B4X:
ws.Session.SetAttribute("frmWelcome",  "false")
    ws.Session.SetAttribute("frmSignUp",  "false")
    ws.Session.SetAttribute("frmSignIn",  "false")
    ws.Session.SetAttribute("frmContactUs",  "false")
    ws.Session.SetAttribute("frmMain",  "false")
    ws.Session.SetAttribute("frmMatesMenu",  "false")
    ws.Session.SetAttribute("frmSchoolMenu",  "false")
    ws.Session.SetAttribute("frmAnnouncement",  "false")
    ws.Session.SetAttribute("frmAnnouncements",  "false")
    ws.Session.SetAttribute("frmEvents",  "false")
    ws.Session.SetAttribute("frmEvent",  "false")
    ws.Session.SetAttribute("frmDonationProjects",  "false")
    ws.Session.SetAttribute("frmDonationProject",  "false")
    ws.Session.SetAttribute("frmBank",  "false")
    ws.Session.SetAttribute("frmActivity",  "false")
    ws.Session.SetAttribute("frmMissing",  "false")
    ws.Session.SetAttribute("frmInMemory",  "false")

When I tested this, it did exactly what I intended, when for example, frmWelcome is opened, it does not need authorization, thus "false", so everything will work. However should I have changed e.g frmBank to "true", if a user would open the frmBank page without authorization, the app will go back to the frmWelcome page.

On frmBank, WebSocket_Connected, this script was added...

B4X:
'page specific authorization
    If ws.Session.GetAttribute2("frmBank", "false") = "true" Then
        If ws.Session.GetAttribute2("IsAuthorized", "") = "" Then
            ABMShared.NavigateToPage(ws, "../")
            Return
        End If
    End If

My approach to build my app was to have most controls created during the BuildPage method as these controls were not going to change during the execution of the project at all. Where controls needed to be updated with data during runtime, I put stuff in ConnectPage.

So, I added the navigation bars

B4X:
' adding a navigation bar
    Dim sbtopimg As ABMImage
    sbtopimg.Initialize(page, "sbtopimg", "../images/320.png", 1)
    sbtopimg.SetFixedSize(49, 49)
    page.NavigationBar.Initialize(page, "navBar", ABM.SIDEBAR_MANUAL_HIDEMEDIUMSMALL, "Sign In", True, True, 330, 48, sbtopimg, ABM.COLLAPSE_ACCORDION, "nav1theme")
    page.NavigationBar.ActiveSideReturnName = "frmSignIn"
    page.NavigationBar.TopBarDropDownConstrainWidth = False
    page.NavigationBar.AddTopItem("SignIn", "", "fa fa-sign-in", "../frmMain/frmMain.html", True)
    page.NavigationBar.AddTopItem("GoToSignUp", "", "fa fa-user-plus", "../frmSignUp/frmSignUp.html", True)
    page.NavigationBar.AddTopItem("ForgotPassword", "", "mdi-action-account-circle", "", True)
    page.NavigationBar.AddTopItem("GoBack", "", "mdi-image-navigate-before", "../frmWelcome/frmWelcome.html", False)
    page.NavigationBar.AddSideBarDivider("")
    page.NavigationBar.AddSideBarItem("frmWelcome", "Welcome", "fa fa-bullseye", "../frmWelcome/frmWelcome.html")
    page.NavigationBar.AddSideBarDivider("")
    page.NavigationBar.AddSideBarItem("frmSignIn", "Sign In", "fa fa-sign-in", "../frmSignIn/frmSignIn.html")
    page.NavigationBar.AddSideBarDivider("")
    page.NavigationBar.AddSideBarItem("frmSignUp", "Sign Up", "fa fa-user-plus", "../frmSignUp/frmSignUp.html")
    page.NavigationBar.AddSideBarDivider("")
    page.NavigationBar.AddSideBarItem("frmContactUs", "Contact Us", "mdi-action-question-answer", "../frmContactUs/frmContactUs.html")

I had to trap the SignIn and ForgotPassword clicks top button as I need it to get the form contents and verify the sign in details. This trapping was done on the Page_NavigationBarClicked method. As I'm still working on this, some methods here will become reduntant and I will remove them, if they dont have code associated with them, e.g."goback", "gotosignup" methods.

B4X:
Public Sub Page_NavigationbarClicked(Action As String, Value As String)
    page.SaveNavigationBarPosition
    If Action = "LogOff" Then
        ABMShared.LogOff(page)
        Return
    End If
    Select Case Action.ToLowerCase
    Case "signin"
        ExecuteSignIn
        Return
    Case "gotosignup"
        ExecuteGoToSignUp
        Return
    Case "forgotpassword"
        ExecuteForgotPassword
        Return
    Case "goback"
        ExecuteGoBack
        Return
    End Select
    ABMShared.NavigateToPage(ws, Value)
End Sub

Then the methods with code processing...

B4X:
Public Sub ExecuteSignIn
    If SignInProcess = False Then Return
    ABMShared.NavigateToPage(ws, "../frmMain/frmMain.html")
End Sub

B4X:
Public Sub ExecuteForgotPassword
    If ForgotPasswordProcess = False Then Return
End Sub
 

Mashiane

Expert
Licensed User
Longtime User
Sign In: Continued

The sign in process reads the user details from the database, validates if the information is complete, validates it against what has been specified and then goes to the next page after the session variables have been set. I have an IsAdmin field and an IsActive fields in my users table to control group permissions to the app. So on login, these are also checked.

B4X:
Private Sub SignInProcess As Boolean
    'define a map to hold the form contents
    Dim m As Map
    'read the file contents to a map
    m = GetContents
    'validate the form contents where required
    If Validate(m) = False Then
        Return False
    End If
    'the form contents are ok, continue with the sign in process
    'define the search criteria, fields must equal
    Dim w As Map
    Dim nk As String
    Dim nv As String
    w.Initialize
    For Each strKey As String In m.Keys
        nv = m.Get(strKey)
        nk = strKey & "="
        w.put(nk, nv)
    Next
    'Get connection from current pool if MySQL/MSSQL
    Dim SQL As SQL = ABMShared.SQLGet
    Dim UserMap As Map = ABMShared.SQLSelectRecordWhereMap(SQL,"users", w)
    'Close the connection to the database
    ABMShared.SQLClose(SQL)
    If UserMap.IsInitialized = False Then
        ShowMsgBox("The Sign In credentials you have provided are incorrect. Please check them and try to sign in again.")
        ws.Session.SetAttribute("IsAuthorized", "")
        Return False
    End If
    Dim uactive As String = UserMap.get("isactive")
    ws.Session.SetAttribute("UserActive", uactive)
    If uactive = 0 Then
        ShowMsgBox("Your Profile is currently in-active, please contact the App Administrator.")
        Return False
    End If
    Dim uemail As String = UserMap.get("email")
    ws.Session.SetAttribute("UserEmail", uemail)
    Dim uid As String = UserMap.get("id")
    ws.Session.SetAttribute("UserID", uid)
    Dim utype As String = UserMap.get("isadmin")
    ws.Session.SetAttribute("UserType", utype)
    ws.Session.SetAttribute("IsAuthorized", "true")
    ws.Session.SetAttribute("authType", "local")
    Return True
End Sub

When a user has forgotten their passwords, they can enter their email address, this will be read from the database, if found and email will be sent to the user with the credentials.

B4X:
Private Sub ForgotPasswordProcess As Boolean
    'define a map to hold the form contents
    Dim m As Map
    'read the form contents to a map
    m = GetContents
    Dim email As String
    email = m.get("email")
    If email = "null" Then email = ""
    If email.Length = 0 Then
        ShowMsgBox("Email cannot be blank. Please enter a value.")
        Return False
    End If
    'the form contents are ok, send login credentials to user
    'define the search criteria, fields must equal
    Dim w As Map
    Dim nk As String
    w.Initialize
    nk = "email="
    w.put(nk, email)
    'Get connection from current pool if MySQL/MSSQL
    Dim SQL As SQL = ABMShared.SQLGet
    Dim UserMap As Map = ABMShared.SQLSelectRecordWhereMap(SQL,"users", w)
    'Close the connection to the database
    ABMShared.SQLClose(SQL)
    If UserMap.IsInitialized = False Then
        ShowMsgBox("The Email specified could not be found in our records, please specify the correct email.")
        Return False
    End If
    'The email has been found, send the details to the user
    Dim password As String
    password = UserMap.get("password")
    Try
        page.Pause
        ' start smtp to send the emails
        smtp.Initialize("...", 25, "...", "...", "smtp")
        smtp.To.Add(email)
        smtp.Body = ABMShared.Replace("Good Day||Someone or you requested your Sign In credentials.||Your Password is: " & password & "||Development Team|", "|", CRLF)
        smtp.Subject = "lfjss: Sign In Credentials"
        smtp.Send
        Return True
    Catch
        page.Resume
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastred", "Email could not be sent, please try again.", 3000)
        Return False
    End Try
End Sub

When the email is sent, if there is an error, a red toast is sent and a green toast shown on success.

B4X:
Public Sub smtp_MessageSent(Success As Boolean)
    page.Resume
    If Success = False Then
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastred", "Email could not be sent, please try again.", 3000)
    Else
        Clear
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastgreen", "Email sent successfully.", 3000)
    End If
End Sub

Please note, with my SQL methods, the operators e.g. "=" are added to the where clause.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Modal Sheets:

The modal sheets used here are based on the ABMaterial demo and feedback examples, so nothing fancy. I just changed one to "confirm" for the yesno clicks and also added a tag property to the buttons as I forso going forward with this using ABMModalSheets instead of single pages, then following an SPA (Single Page Approach). I will see with that.

B4X:
Public Sub BuildMsgBox() As ABMModalSheet
    Dim msgbox As ABMModalSheet
    msgbox.Initialize(page, "msgbox", False, False, "")
    msgbox.IsDismissible = False
    msgbox.Content.AddRowsM(1, True,0,0, "").AddCells12(1, "")
    msgbox.Content.BuildGrid
    ' add paragraph
    msgbox.Content.CellR(0,1).AddComponent(ABMShared.BuildParagraph(page,"par1","message") )
    msgbox.Footer.AddRowsM(1,True,0,0, "").AddCellsOS(1,6,6,6,3,3,3,"").AddCellsOS(1,0,0,0,3,3,3, "")
    msgbox.Footer.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
    ' create the buttons for the footer
    Dim msgok As ABMButton
    msgok.InitializeFlat(page, "msgok", "", "", "CLOSE", "transparent")
    msgbox.Footer.Cell(1,1).AddComponent(msgok)
    Return msgbox
End Sub
Public Sub BuildMsgBoxYesNo() As ABMModalSheet
    Dim msg As ABMModalSheet
    msg.Initialize(page, "confirm", False, False, "")
    msg.IsDismissible = False
    msg.Content.AddRowsM(1, True,0,0, "").AddCells12(1, "")
    msg.Content.BuildGrid
    ' add paragraph
    msg.Content.CellR(0,1).AddComponent(ABMShared.BuildParagraph(page,"par1","Are you sure you want to delete this user?") )
    msg.Footer.AddRowsM(1,True,0,0, "").AddCellsOS(1,6,6,6,3,3,3,"").AddCellsOS(1,0,0,0,3,3,3, "")
    msg.Footer.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
    ' create the buttons for the footer
    Dim msgyes As ABMButton
    msgyes.InitializeFlat(page, "msgyes", "", "", "YES", "transparent")
    msg.Footer.Cell(1,1).AddComponent(msgyes)
    Dim msgno As ABMButton
    msgno.InitializeFlat(page, "msgno", "", "", "NO", "transparent")
    msg.Footer.Cell(1,2).AddComponent(msgno)
    Return msg
End Sub
Public Sub BuildWrongInputModalSheet() As ABMModalSheet
    Dim myModalError As ABMModalSheet
    myModalError.Initialize(page, "wronginput", False, False, "")
    myModalError.Content.UseTheme("modalcontent")
    myModalError.Footer.UseTheme("modalcontent")
    myModalError.IsDismissible = True
    ' create the grid for the content
    myModalError.Content.AddRows(1,True, "").AddCells12(1,"")
    myModalError.Content.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
    Dim lbl1 As ABMLabel
    lbl1.Initialize(page, "errormsg", "The login or password are incorrect!",ABM.SIZE_PARAGRAPH, False, "")
    myModalError.Content.Cell(1,1).AddComponent(lbl1)
    Return myModalError
End Sub
Public Sub ShowMsgBox(strMessage As String)
    Dim msgbox As ABMModalSheet = page.ModalSheet("msgbox")
    Dim lbl As ABMLabel = msgbox.Content.Component("par1")
    lbl.Text = strMessage
    page.ShowModalSheet("msgbox")
End Sub
Public Sub ShowYesNo(strMessage As String, strYes As String, strNo As String)
    Dim msg As ABMModalSheet = page.ModalSheet("confirm")
    Dim lbl As ABMLabel = msg.Content.Component("par1")
    Dim msgyes As ABMButton = msg.Footer.Component("msgyes")
    Dim msgno As ABMButton = msg.Footer.Component("msgno")
    lbl.Text = strMessage & "?"
    msgyes.Tag = strYes
    msgno.Tag = strNo
    page.ShowModalSheet("confirm")
End Sub
Public Sub msgyes_Clicked(Target As String)
    Dim msg As ABMModalSheet = page.ModalSheet("confirm")
    Dim msgyes As ABMButton = msg.Footer.Component("msgyes")
    Dim strTag As String
    strTag = msgyes.Tag
    page.CloseModalSheet("confirm")
End Sub
Public Sub msgok_Clicked(Target As String)
    page.CloseModalSheet("msgbox")
End Sub
Public Sub msgno_Clicked(Target As String)
    page.CloseModalSheet("confirm")
End Sub
Public Sub ShowInputError(strMessage As String)
    Dim mymodalError As ABMModalSheet = page.ModalSheet("wronginput")
    Dim myModalLbl As ABMLabel= mymodalError.Content.Component("errormsg")
    myModalLbl.Text = strMessage
    page.ShowModalsheet("wronginput")
End Sub
 

Mashiane

Expert
Licensed User
Longtime User
Helper Methods:

I will be using the same approach for other pages also, so GetContents will read the form components and save the details in a map. Validate will validate all the required fields in my page. As this is a page on its own, there will be no conflict with methods here, I forsee that when using ABMModalSheets, I will have to "prefix" or "suffix" each of these methods.

B4X:
'Get the contents of the page input components and save to a map
Public Sub GetContents() As Map
    Dim pMap As Map
    pMap.Initialize
    Dim txtEmail As ABMInput = page.Component("txtEmail")
    pMap.put("email", txtEmail.Text)
    Dim txtPassword As ABMInput = page.Component("txtPassword")
    pMap.put("password", txtPassword.Text)
    Return pMap
End Sub
'Get the contents of the page from GetContents and check validity
Public Sub Validate(gMap As Map) As Boolean
    Dim stxtEmail As String
    stxtEmail = gMap.get("email")
    If stxtEmail = "null" Then stxtEmail = ""
    If stxtEmail.Length = 0 Then
        ShowMsgBox("Email cannot be blank. Please enter a value.")
        Dim txtEmail As ABMInput = page.Component("txtEmail")
        txtEmail.SetFocus
        Return False
    End If
    Dim stxtPassword As String
    stxtPassword = gMap.get("password")
    If stxtPassword = "null" Then stxtPassword = ""
    If stxtPassword.Length = 0 Then
        ShowMsgBox("Password cannot be blank. Please enter a value.")
        Dim txtPassword As ABMInput = page.Component("txtPassword")
        txtPassword.SetFocus
        Return False
    End If
    Return True
End Sub

Database Access:

I updated my ABMShared module with extra methods to take care of the database functionality. These are used accross the app as a whole. As you will see, if you have used DBUtils, there is almost 100% similarity with a few tweaks of my own here and there per forum advise e.g. use of transactions and other things from the ABMGenerator, it works...

In my AppStart, as expected, the database initialization happened..

B4X:
 ABMShared.InitializeMySQL("localhost", "3306", "dbname", "username", "password!", 100)

Defining the connection string...

B4X:
'Define a connection to MySQL server
Public Sub MySQLConnectionString(serverIP As String,  serverPort As String, serverDB As String) As String
    Dim sb As StringBuilder
    sb.Initialize
    sb.Append("jdbc:mysql://").Append(serverIP).Append(":").Append(serverPort).Append("/").Append(serverDB)
    Return sb.tostring
End Sub

B4X:
'Initialize a connection to MySQL server and returns True if successful
Public Sub InitializeMySQL(serverIP As String,  serverPort As String, serverDB As String, login As String, password As String, poolSize As Int) As Boolean
    Log("init mysql")
    DatabaseType = 1
    UsePool = True
    Try
        Dim jdbcUrl As String
        jdbcUrl = MySQLConnectionString(serverIP,serverPort,serverDB)
        pool.Initialize("com.mysql.jdbc.Driver", jdbcUrl, login, password)
        ' change pool size...
        Dim jo As JavaObject = pool
        jo.RunMethod("setMaxPoolSize", Array(poolSize))
        Return True
    Catch
        Log("Last Pool Init Except: "&LastException.Message)
        Return False
    End Try
End Sub

'Update a single mapped record and return true if successful
Public Sub SQLRecordUpdate(SQL As SQL, TableName As String, Fields As Map, PrimaryKey As String, PrimaryValue As String) As Boolean
    Dim w As Map
    w.Initialize
    w.Put(PrimaryKey, PrimaryValue)
    Return SQLRecordUpdateWhere(SQL,TableName,Fields,w)
End Sub
'Update a mapped record and return true if successful
Public Sub SQLRecordUpdateWhere(SQL As SQL, TableName As String, Fields As Map, WhereFieldEquals As Map) As Boolean
    If WhereFieldEquals.Size = 0 Then
        Log("WhereFieldEquals map empty!")
        Return False
    End If
    If Fields.Size = 0 Then
        Log("Fields empty")
        Return False
    End If
    Dim sb As StringBuilder
    SQL.BeginTransaction
    Try
        sb.Initialize
        sb.Append("UPDATE ").Append(TableName).Append(" SET ")
        Dim args As List
        args.Initialize
        For i=0 To Fields.Size-1
            If i<>Fields.Size-1 Then
                sb.Append(Fields.GetKeyAt(i)).Append("=?,")
            Else
                sb.Append(Fields.GetKeyAt(i)).Append("=?")
            End If
            args.Add(Fields.GetValueAt(i))
        Next
        sb.Append(" WHERE ")
        For i = 0 To WhereFieldEquals.Size - 1
            If i > 0 Then
                sb.Append(" AND ")
            End If
            sb.Append(WhereFieldEquals.GetKeyAt(i)).Append(" = ?")
            args.Add(WhereFieldEquals.GetValueAt(i))
        Next
        Log("UpdateRecord: " & sb.ToString)
        SQL.ExecNonQuery2(sb.ToString, args)
        SQL.TransactionSuccessful
        Return True
    Catch
        Log("UpdateRecord2 Error: " & LastException)
        SQL.Rollback
        Return False
    End Try
End Sub
'Insert a single map record and return true if successful
Public Sub SQLRecordInsert(SQL As SQL, TableName As String, nRecord As Map) As Int
    Dim nList As List
    nList.Initialize
    nList.Add(nRecord)
    Return SQLRecordInsertMaps(SQL, TableName, nList)
End Sub
'Insert Mapped Records to database and return true if successful
Public Sub SQLRecordInsertMaps(SQL As SQL, TableName As String, ListOfMaps As List) As Int
    Dim res As Int
    Dim sb, columns, values As StringBuilder
    'Small check for a common error where the same map is used in a loop
    If ListOfMaps.Size > 1 And ListOfMaps.Get(0) = ListOfMaps.Get(1) Then
        Log("Same Map found twice in list. Each item in the list should include a different map object.")
        Return DBERROR
    End If
    SQL.BeginTransaction
    Try
        For i1 = 0 To ListOfMaps.Size - 1
            sb.Initialize
            columns.Initialize
            values.Initialize
            Dim listOfValues As List
            listOfValues.Initialize
            sb.Append("INSERT INTO " & TableName & " (")
            Dim m As Map
            m = ListOfMaps.Get(i1)
            For i2 = 0 To m.Size - 1
                Dim col As String
                Dim value As Object
                col = m.GetKeyAt(i2)
                value = m.GetValueAt(i2)
                If i2 > 0 Then
                    columns.Append(", ")
                    values.Append(", ")
                End If
                columns.Append(col)
                values.Append("?")
                listOfValues.Add(value)
            Next
            sb.Append(columns.ToString)
            sb.Append(") VALUES (")
            sb.Append(values.ToString)
            sb.Append(")")
            If i1 = 0 Then Log("InsertMaps (first query out of " & ListOfMaps.Size & "): " & sb.ToString)
            Log("InsertMaps: " & sb.ToString)
            SQL.ExecNonQuery2(sb.ToString, listOfValues)
        Next
        SQL.TransactionSuccessful
        If UsePool Then
            res = SQLSelectSingleResult(SQL, "SELECT LAST_INSERT_ID()",Null)
        Else
            res = SQLSelectSingleResult(SQL, "SELECT last_insert_rowid()",Null)
        End If
        Return res
    Catch
        Log("InsertMaps Error: " & LastException)
        SQL.Rollback
        Return DBERROR
    End Try
End Sub
'Get a connection to SQL server
Public Sub SQLGet() As SQL
    If DatabaseType = 0 Then
        Return SQLite
    Else
        Return pool.GetConnection
    End If
End Sub
'Close the database connection
Public Sub SQLClose(mySQL As SQL)
    If DatabaseType <> 0 Then
        mySQL.Close
    End If
End Sub
'Determines if a record exists and returns true
Public Sub SQLRecordExists(SQL As SQL, TableName As String, PrimField As String, PrimValue As String) As Boolean
    Dim mr As Map = SQLExecuteMap(SQL, "SELECT " & PrimField & " From " & TableName & " WHERE " & PrimField & " = ?", Array As String(PrimValue))
    If mr.IsInitialized = False Then
        Return False
    Else
        Return True
    End If
End Sub
'Execute a select query and returns a single record
Public Sub SQLExecuteMap(SQL As SQL, Query As String, StringArgs() As String) As Map
    Dim res As Map
    Dim cur As ResultSet
    If StringArgs <> Null Then
        cur = SQL.ExecQuery2(Query, StringArgs)
    Else
        cur = SQL.ExecQuery(Query)
    End If
    Log("ExecuteMap: " & Query)
    If cur.NextRow = False Then
        Log("No records found.")
        Return res
    End If
    res.Initialize
    For i = 0 To cur.ColumnCount - 1
        res.Put(cur.GetColumnName(i).ToLowerCase, cur.GetString2(i))
    Next
    cur.Close
    Return res
End Sub
'Execute a query and return a list of Mapped records
Public Sub SQLExecuteMaps(SQL As SQL, Query As String, args As List) As List
    Dim l As List
    l.Initialize
    Dim cur As ResultSet
    Try
        cur = SQL.ExecQuery2(Query, args)
    Catch
        Log(LastException)
        Return l
    End Try
    Do While cur.NextRow
        Dim res As Map
        res.Initialize
        For i = 0 To cur.ColumnCount - 1
            res.Put(cur.GetColumnName(i).ToLowerCase, cur.GetString2(i))
        Next
        l.Add(res)
    Loop
    cur.Close
    Return l
End Sub
'Read a record from the database and return it as a map
Public Sub SQLRecordRead(SQL As SQL, TableName As String, PrimaryKey As String, PrimaryValue As String) As Map
    Return SQLExecuteMap(SQL, "SELECT * FROM " & TableName & " WHERE " & PrimaryKey & " = ?",Array As String(PrimaryValue))
End Sub
'Delete single/multiple records specifying a primary key
Public Sub SQLRecordDelete(SQL As SQL, TableName As String, KeyField As String, KeyValue As String) As Boolean
    Dim w As Map
    w.Initialize
    w.Put(KeyField,KeyValue)
    Return SQLRecordDeleteWhere(SQL,TableName,w)
End Sub
Public Sub SQLRecordDeleteWhere(SQL As SQL, TableName As String, WhereFieldEquals As Map) As Boolean
    Dim sb As StringBuilder
    sb.Initialize
    sb.Append("DELETE FROM ").Append(TableName).Append(" WHERE ")
    If WhereFieldEquals.Size = 0 Then
        Log("WhereFieldEquals map empty!")
        Return False
    End If
    Dim args As List
    args.Initialize
    For i = 0 To WhereFieldEquals.Size - 1
        If i > 0 Then sb.Append(" AND ")
        sb.Append(WhereFieldEquals.GetKeyAt(i)).Append(" = ?")
        args.Add(WhereFieldEquals.GetValueAt(i))
    Next
    Log("DeleteRecord: " & sb.ToString)
    SQL.BeginTransaction
    Try
        SQL.ExecNonQuery2(sb.ToString, args)
        SQL.TransactionSuccessful
        Return True
    Catch
        SQL.Rollback
        Return False
    End Try
End Sub
Public Sub SQLSelectSingleResult(SQL As SQL, Query As String, Args As List) As String
    Dim res As String
    Try
        res = SQL.ExecQuerySingleResult2(Query, Args)
    Catch
        Log(LastException)
        res = DBERROR
    End Try
    If res = Null Then
        res = DBNORESULTS
    End If
    Return res
End Sub

Please note: You can use the same approach as per ABMGenerator, this is just my approach to this, I'm not saying it's better, it works for me.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
The Contact Us Form

The contact us form is not so different from the Sign In page. A user is provided with details to enter on a page and once they finish entering the content, an email is sent to a specified email address.

ContactUs.png


I created the page components like....

B4X:
Dim txtFullName As ABMInput
    txtFullName.Initialize(page, "txtFullName", ABM.INPUT_TEXT, "Full Name", False, "inputtheme")
    txtFullName.IconName = "fa fa-user"
    page.Cell(2,1).AddComponent(txtFullName)
    Dim txtEmail As ABMInput
    txtEmail.Initialize(page, "txtEmail", ABM.INPUT_EMAIL, "Email", False, "inputtheme")
    txtEmail.WrongMessage = "Invalid"
    txtEmail.SuccessMessage = "Valid"
    txtEmail.IconName = "fa fa-envelope-o"
    page.Cell(3,1).AddComponent(txtEmail)
    Dim txtMobilePhone As ABMInput
    txtMobilePhone.Initialize(page, "txtMobilePhone", ABM.INPUT_TEL, "Mobile Phone", False, "inputtheme")
    txtMobilePhone.IconName = "mdi-communication-phone"
    page.Cell(4,1).AddComponent(txtMobilePhone)
    Dim txtComment As ABMInput
    txtComment.Initialize(page, "txtComment", ABM.INPUT_TEXT, "Comment", True, "inputtheme")
    txtComment.IconName = "mdi-communication-comment"
    page.Cell(5,1).AddComponent(txtComment)
    Dim frmContactUsContactUs As ABMActionButton
    frmContactUsContactUs.Initialize(page, "frmContactUsContactUs", "mdi-action-question-answer", "bigblue")
    frmContactUsContactUs.MainButton.Size = ABM.BUTTONSIZE_LARGE
    page.AddActionButton(frmContactUsContactUs)

All the details of the Contast Us form are required..

B4X:
Public Sub Validate(gMap As Map) As Boolean
    Dim stxtFullName As String
    stxtFullName = gMap.get("fullname")
    If stxtFullName = "null" Then stxtFullName = ""
    If stxtFullName.Length = 0 Then
        ShowMsgBox("Full Name cannot be blank. Please enter a value.")
        Dim txtFullName As ABMInput = page.Component("txtFullName")
        txtFullName.SetFocus
        Return False
    End If
    Dim stxtEmail As String
    stxtEmail = gMap.get("email")
    If stxtEmail = "null" Then stxtEmail = ""
    If stxtEmail.Length = 0 Then
        ShowMsgBox("Email cannot be blank. Please enter a value.")
        Dim txtEmail As ABMInput = page.Component("txtEmail")
        txtEmail.SetFocus
        Return False
    End If
    Dim stxtMobilePhone As String
    stxtMobilePhone = gMap.get("mobilephone")
    If stxtMobilePhone = "null" Then stxtMobilePhone = ""
    If stxtMobilePhone.Length = 0 Then
        ShowMsgBox("Mobile Phone cannot be blank. Please enter a value.")
        Dim txtMobilePhone As ABMInput = page.Component("txtMobilePhone")
        txtMobilePhone.SetFocus
        Return False
    End If
    Dim stxtComment As String
    stxtComment = gMap.get("comment")
    If stxtComment = "null" Then stxtComment = ""
    If stxtComment.Length = 0 Then
        ShowMsgBox("Comment cannot be blank. Please enter a value.")
        Dim txtComment As ABMInput = page.Component("txtComment")
        txtComment.SetFocus
        Return False
    End If
    Return True
End Sub

And when the action button or topbar button are clicked, this will fire this method..

B4X:
Private Sub ContactUsProcess
    'define a map to hold the form contents
    Dim m As Map
    'read the file contents to a map
    m = GetContents
    'validate the form contents where required
    If Validate(m) = False Then
        Return
    End If
    Dim fullname As String = m.get("fullname")
    Dim email As String = m.get("email")
    Dim mobilephone As String = m.get("mobilephone")
    Dim comment As String = m.get("comment")
    Try
        page.Pause
        'the form contents are ok, continue with the email send
        ' start smtp to send the emails
        smtp.Initialize("xxx", 25, "username", "password", "smtp")
        smtp.To.Add("[email protected]")
        smtp.Body = ABMShared.Replace("FullName: " & fullname & "||Email: " & email & "||MobilePhone: " & mobilephone & "||Comment: " & comment & "||", "|", CRLF)
        smtp.Subject = "lfjss: Contact Us"
        smtp.Send
    Catch
        page.Resume
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastred", "Email could not be sent, please try again.", 3000)
    End Try
End Sub

You can refer to the smtp_ method above for email sent confirmation.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
The Welcome Page

The Welcome page is the start of the app, and shows when the Little Flower App is shown. It provides access for one to Sign In, Sign Up and also Contact Us.

WelcomePage.png


With this page, I explored the Footer, and SocialShare. I used the already provided BuildHeader and BuildParagraph. I decided my Footer to be floating too as it would appear properly in small devices.

B4X:
 page.SocialShareTheme = ABM.SOCIALSHARTHEMETYPE_CLASSIC
    Dim socshare261 As ABMSocialShare
    socshare261.Initialize(page, "socshare261", "host address", Null, "Little Flower JSS WebApp", ABM.SOCIALSHAREBUTTONTYPE_BUTTONLABELCOUNT)
    socshare261.AddEmail
    socshare261.AddFacebook
    socshare261.AddGooglePlus
    socshare261.AddLinkedIn
    socshare261.AddTwitter("LittleFlowerJSS", "anelembanga")
    socshare261.AddWhatsApp
    page.Cell(3,1).AddComponent(socshare261)

In my Android App, the contents of the welcome page should be dynamic as an administrator should be able to change the details.

B4X:
page.Cell(1,1).AddComponent(ABMShared.BuildHeader(page, "par209", "Welcome to Little Flower JSS ReUnion App"))
    page.Cell(1,1).AddComponent(ABMShared.BuildParagraph(page, "par262", "Molo Ngqonda! Ewe uliNgqonda kwabe ukwazi ukungena apha! Halala! {BR}{BR}This app is meant to be a single reference point for all the reunion activities that have been discussed and planned in the WhatsApp group chats and the regional meetings that have taken place. This is not to replace the WhatsApp group chats…..so xa ugqibile apha phindela emanxiweni! {BR}{BR}The plan is for us all to have our first reunion eNgqonda last weekend of Sept.(30 Sept to 02nd Oct).  Permission has been granted eskolweni and this will be during school holidays so we can occupy the hostel. Check-in will be Friday and departure on Sunday. {BR}{BR}We each are contributing a non-compulsory minimum of R500 towards refurbishing the school and/or whatever their needs are which we are slowly establishing. There is an account given to us by the school in which to pay in our contributions and we each have to send proof of payment to the email address [email protected] and this is the same email to which we RSVP for the reunion. {BR}{BR}The reunion is going to be really a social and networking gathering more than anything else however we will have another big chat session there to discuss how we can further be of help to Little Flower and we plan to have some ongoing meetings for those who'd still love to walk the Little Flower journey further even after the fun weekend. We will discuss fund raising options etc..{BR}During that weekend we also plan to honour our fallen heroes right from our very own Sister Da, teachers and all our brothers and sisters, may all their souls know peace. {BR}So yeah....that's really it for now, watch this space for ✊{BR}{BR}A ‘huge thanks’ to our organizers, the EXCO:{BR}Cinga Siwundla – The Queen{BR}Baphiwe Mtirara – COO{BR}Unathi Mgobhozi - CE{BR}Nomahlubi Mbambisa{BR}Tabisile Mapheleba{BR}{BR}Enkosi."))

For this to work, I noted that all CRLFs should be changed to {BR}. The smileys and other things, well this was a message that was copied as is from WhatsApp.

Going forward I want to enable a user have an update section where this content will be updated. In my Android App, I have designed a screen, a user types in the welcome note screen, this gets saved to a file, this file is uploaded to the server and then read back each time a user accesses the Welcome screen and there this welcome note is accessible once a person has signed in.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
The Main Menu

This will be the central entry point for everything the app does. This ABMList is built from static items and it enables one to access other dynamic ABMLists e.g. Events, Donations etc.

MainMenu.png


When creating the page grid here, I specified the method to False for center in page.

B4X:
 ' create the page grid
    page.AddRowsM(5, False, 0, 0, "").AddCellsOSMP(1, 0, 0, 0, 12, 12, 12, 0, 0, 0, 0, "")

As I wanted the same look as the Android App, the list to fill the screen. Then I added each item in the list.

B4X:
Dim lst271 As ABMList
    lst271.Initialize(page, "lst271", ABM.COLLAPSE_EXPANDABLE, "greylist")
    lst271.AddItemWithTheme("01MyProFile", ABMShared.ListItemImageTitleDescription(page, "01MyProFile", "../images/fa-user.png", "{B}My Profile{B}", "Please update your profile", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("02Announcements", ABMShared.ListItemImageTitleDescription(page, "02Announcements", "../images/fa-newspaper-o.png", "{B}Announcements{B}", "Latest announcements", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("03Events", ABMShared.ListItemImageTitleDescription(page, "03Events", "../images/fa-calendar.png", "{B}Events{B}", "Programme of Events", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("04Donations", ABMShared.ListItemImageTitleDescription(page, "04Donations", "../images/fa-money.png", "{B}Donations{B}", "Money that has been donated", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("05SchoolMates", ABMShared.ListItemImageTitleDescription(page, "05SchoolMates", "../images/fa-users.png", "{B}School Mates{B}", "See your school mates", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("06Gallery", ABMShared.ListItemImageTitleDescription(page, "06Gallery", "../images/fa-file-image-o.png", "{B}Gallery{B}", "School image gallery", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("07SchoolInformation", ABMShared.ListItemImageTitleDescription(page, "07SchoolInformation", "../images/fa-building-o.png", "{B}School Information{B}", "School information", "", "", False, "greycontainer"), "greyitem")
    lst271.AddItemWithTheme("08ContactUs", ABMShared.ListItemImageTitleDescription(page, "08ContactUs", "../images/fa-envelope-o.png", "{B}Contact Us{B}", "Send comments, bugs, requests etc", "", "", False, "greycontainer"), "greyitem")
    page.Cell(1,1).AddComponent(lst271)

For this to have this look and feel of grey, I created a greycontainer and a greyitem theme and adjusted the example code to add items to the combo box to ListItemImageTitleDescription.

B4X:
Public Sub ListItemImageTitleDescription(page As ABMPage, id As String, image As String, Title As String, Subtitle As String, TitleTheme As String, DescriptionTheme As String, bIsCircular As Boolean, ContainerTheme As String) As ABMContainer
    Dim ItemCont As ABMContainer
    ItemCont.Initialize(page, id, ContainerTheme)
    ItemCont.AddRowsM(1,False,6,0, "").AddCellsOSMP(1,0,0,0,3,2,2,0,0,16,0,"").AddCellsOS(1,0,0,0,9,10,10,"")
    ItemCont.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
    Dim SubItemCont As ABMContainer
    SubItemCont.Initialize(page, id & "SubItemCont", ContainerTheme)
    SubItemCont.AddRowsM(1,False, 0,0,"").AddCells12MP(1,-6,0,0,0,"").AddCells12(1,"")
    SubItemCont.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components
    ItemCont.Cell(1,2).AddComponent(SubItemCont)
    Dim img As ABMImage
    img.Initialize(page, id & "img", image, 1)
    img.SetFixedSize(48,48)
    img.IsCircular = bIsCircular
    img.IsResponsive = True
    ItemCont.Cell(1,1).AddComponent(img)
    Dim lbl1 As ABMLabel
    lbl1.Initialize(page, id & "lbl1", Title, ABM.SIZE_H6, False, TitleTheme)
    lbl1.VerticalAlign = True
    SubItemCont.Cell(1,1).AddComponent(lbl1)
    Dim lbl2 As ABMLabel
    lbl2.Initialize(page, id & "lbl2", Subtitle, ABM.SIZE_H6, False, DescriptionTheme)
    lbl2.VerticalAlign = True
    SubItemCont.Cell(1,2).AddComponent(lbl2)
    Return ItemCont
End Sub

The greycontainer theme is applied to the internal container inside the item.

B4X:
MyTheme.AddContainerTheme("greycontainer")
    MyTheme.Container("greycontainer").backcolor = ABM.COLOR_GREY
    MyTheme.Container("greycontainer").backcolorintensity = ABM.INTENSITY_LIGHTEN4

and each item in the list, gets the greyitem applied to it.

B4X:
MyTheme.List("greylist").AddItemTheme("greyitem")
    MyTheme.List("greylist").Item("greyitem").activecolor = ABM.COLOR_GREY
    MyTheme.List("greylist").Item("greyitem").hovercolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").Item("greyitem").backcolor = ABM.COLOR_GREY
    MyTheme.List("greylist").Item("greyitem").activecolorintensity = ABM.INTENSITY_LIGHTEN4
    MyTheme.List("greylist").Item("greyitem").backcolorintensity = ABM.INTENSITY_LIGHTEN4
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
Dynamic ABMList

When you open the School Mates item, another ABMList gets opened. This provides access to school mates details like this. One of the challenging things I faced was generating the dynamic lists for these.

As an example, selecting In Memory, that has to go to the database and select all school mates who passed on and list them. On the same screen you need access to be able to add school mates etc.

SchoolMenu.png


As each school mate has to specify their city, I intend just like the Android version to draw their locations using ABMGoogleMap (as soon as I learn that).

InMemory.png


As I am still developing this, the Action Buttons for medium and large devices will be hidden as the TopBar items have the same code linked to those.

So in BuildPage, I created my ABMList

B4X:
Dim lstInMemory As ABMList
    lstInMemory.Initialize(page, "lstInMemory", ABM.COLLAPSE_EXPANDABLE, "greylist")
    page.Cell(1,1).AddComponent(lstInMemory)

Then in ConnectPage I call a method to load the list based on database content.

B4X:
Private Sub RefreshOnLoad_lstInMemory
    'Clear the contents of the ABMList
    Dim lstInMemory As ABMList
    lstInMemory = page.Component("lstInMemory")
    lstInMemory.Clear
    'Define list to store the results of the query
    Dim results As List
    Dim resCnt As Int
    Dim resTot As Int
    Dim resMap As Map
    Dim sTitles As StringBuilder
    Dim sDescriptions As StringBuilder
    'variable to hold the primary key
    Dim strid As String
    'variables to title fields
    Dim strfirstname As String
    Dim strlastname As String
    'Get connection from current pool if MySQL/MSSQL
    Dim SQL As SQL = ABMShared.SQLGet
    results = ABMShared.SQLExecuteMaps(SQL,"SELECT * FROM users WHERE inmemory = 'Y' ORDER BY graduationyear DESC,grade DESC,firstname,lastname", Null)
    'Loop throught each record read and process it
    resTot = results.size - 1
    For resCnt = 0 To resTot
        sTitles.Initialize
        sDescriptions.Initialize
        'Get the record map
        resMap = results.get(resCnt)
        'process the primary key fields
        strid = resMap.get("id")
        strid = strid.Replace(CRLF,"{BR}")
        'process the title fields
        strfirstname = resMap.get("firstname")
        strfirstname = strfirstname.Replace(CRLF,"{BR}")
        strfirstname = strfirstname & "{NBSP}{NBSP}"
        sTitles.Append(strfirstname)
        strlastname = resMap.get("lastname")
        strlastname = strlastname.Replace(CRLF,"{BR}")
        strlastname = strlastname & "{NBSP}{NBSP}"
        sTitles.Append(strlastname)
        'add the item to the ABMList
        lstInMemory.AddItemWithTheme(strid, ABMShared.ListItemImageTitleDescription(page, strid, "../images/fa-user.png", "{B}"&sTitles.ToString&"{B}", sDescriptions.ToString, "", "", False, "greycontainer"), "greyitem")
    Next
    lstInMemory.Refresh
    'Close the connection to the database
    ABMShared.SQLClose(SQL)
End Sub

The database closing code can be moved to be just after the call to get the results as the list is a memory list. Also note the use of AddItemWithTheme that enables the theming of the list items.
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
CRUD Functionality

CRUD functionality is for Creating new records, Reading Existing Records, Updating existing records and Deleting existing records. Whilst the ABMGenerator uses a ABMTable approach for listing, selecting and reading records, my approach is based on ABMList. So this mean that..

1. Ability to add new records from a list is added.
2. Ability to read a record based on a list selection is added.
3. Ability to update an existing records is added and
4. Ability to delete an existing records is also added.

Creating, Reading and Updating

In the ABMList, for small devices, the ActionButton is provided with Add functionality. Once selected this records that the action to be performed is an "ADD". When the screen to navigate to is opened, then the components are cleared and readied for new input. My approach is using LocalStorage to keep track of these actions.

This method is in the announcements listing, called when Add Record is selected.

B4X:
Public Sub ExecuteNewRecord
    ABMShared.LocalStorageSave(page, "action", "new")
    ABMShared.LocalStorageSave(page, "id", "-1")
    ABMShared.NavigateToPage(ws, "../frmAnnouncement/frmAnnouncement.html")
End Sub

On frmAnnouncements when an item is tapped in the ABMList

B4X:
Public Sub lstAnnouncements_Clicked(ItemId As String)
    Select Case ItemId
    End Select
    ABMShared.LocalStorageSave(page, "action", "edit")
    ABMShared.LocalStorageSave(page, "id", ItemId)
    ABMShared.NavigateToPage(ws, "../frmAnnouncement/frmAnnouncement.html")
End Sub

This uses the id of the record and saves it in LocalStorage and opens frmAnnouncement for Edit.

So when the frmAnnouncement form is displayed, the components are created in BuildPage and on ConnectPage.

B4X:
Public Sub ConnectPage()

    'add components for the page
    Dim strAction As String
    Dim strID As String
    strAction = ABMShared.LocalStorageRead(page, "action")
    strID = ABMShared.LocalStorageRead(page, "id")
    Select Case strAction
    Case "new"
        'clear the contents of the page
        Clear
    Case "edit"
        'clear the contents of the page
        Clear
        'read the record from the database.
        Dim SQL As SQL = ABMShared.SQLGet
        Dim Record As Map
        Record = ABMShared.SQLRecordRead(SQL,"news", "id", strID)
        ABMShared.SQLClose(SQL)
        If Record.IsInitialized = True Then
            SetContents(Record)
        End If
    End Select
    page.Refresh ' IMPORTANT
    ' NEW, because we use ShowLoaderType=ABM.LOADER_TYPE_MANUAL
    page.FinishedLoading 'IMPORTANT
End Sub

If the action is new, clear the contents of the form so that a user can add a new record and save it. If we are in Edit mode, read the records from the database and display it.

The SetContents methods just read the map record extracted from the database and displays it.

B4X:
'Set the contents of the page input components from map
Public Sub SetContents(pMap As Map)
    Dim txtTitle As ABMInput = page.Component("txtTitle")
    Dim txtWrittenBy As ABMInput = page.Component("txtWrittenBy")
    Dim txtContent As ABMInput = page.Component("txtContent")
    Dim dtDatePublished As ABMDateTimePicker = page.Component("dtDatePublished")
    Dim txtTitleContents As String
    If pMap.GetDefault("title",Null) = Null Then
    Else
        txtTitleContents = pMap.get("title")
    End If
    txtTitle.Text = txtTitleContents
    txtTitle.Refresh
    Dim txtWrittenByContents As String
    If pMap.GetDefault("writtenby",Null) = Null Then
    Else
        txtWrittenByContents = pMap.get("writtenby")
    End If
    txtWrittenBy.Text = txtWrittenByContents
    txtWrittenBy.Refresh
    Dim txtContentContents As String
    If pMap.GetDefault("content",Null) = Null Then
    Else
        txtContentContents = pMap.get("content")
    End If
    txtContent.Text = txtContentContents
    txtContent.Refresh
    Dim dtDatePublishedContents As String
    If pMap.GetDefault("datepublished",Null) = Null Then
        dtDatePublishedContents = DateTime.Now
    Else
        dtDatePublishedContents = pMap.get("datepublished")
        dtDatePublishedContents = ABM.Util.ConvertFromDateTimeString(dtDatePublishedContents, "yyyy-MM-dd HH:mm")
    End If
    dtDatePublished.SetDate(dtDatePublishedContents)
    dtDatePublished.Refresh
End Sub

When the Update button is clicked, depending of what mode we are in, whether Add/Edit, the CreateUpdate method is called.

B4X:
Private Sub CreateUpdate
    'define a map to hold the form contents
    Dim m As Map
    'read the file contents to a map
    m = GetContents
    'validate the form contents where required
    If Validate(m) = False Then
        Return
    End If
    'the form contents are ok, continue with the save
    Dim strAction As String
    Dim strID As String
    'determine the action we have been performing
    strAction = ABMShared.LocalStorageRead(page, "action")
    strID = ABMShared.LocalStorageRead(page, "id")
    Select Case strAction
    Case "new"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        'insert the record
        strID = ABMShared.SQLRecordInsert(SQL, "news", m)
        If strID > 0 Then
            ABMShared.LocalStorageSave(page, "action", "edit")
            ABMShared.LocalStorageSave(page, "id", strID)
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record added successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be added, please try again.", 3000)
        End If
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
    Case "edit"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        If ABMShared.SQLRecordUpdate(SQL,"news", m, "id", strID) = True Then
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record updated successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be updated, please try again.", 3000)
        End If
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
    End Select
End Sub

What this does is to read the screen contents, assign these to a map, validate it if necessary, if insert, add the record to the database and turn the mode to edit. If the record already exists, this is updated. In all these instances, a toast is shown on the success of failure of the methods.

Deleting

To delete a record, the operation should be in Edit mode. I intend to hide and show these buttons depending on the mode.

B4X:
Public Sub ExecuteDeleteRecordButton
    'delete only if we are in edit mode, else show toast
    Dim strAction As String
    Dim strID As String
    'determine the action we have been performing
    strAction = ABMShared.LocalStorageRead(page, "action")
    strID = ABMShared.LocalStorageRead(page, "id")
    Select Case strAction
    Case "edit"
        'show msgboxyesno for action
        ActiveID = strID
        ShowYesNo("Are you sure that you want to delete this record? You will not be able to undo your actions. Continue", "DeleteRecord", "CancelDeleteRecord")
    Case Else
        'the record is not in edit mode
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastred", "Record cannot be deleted in this mode.", 3000)
    End Select
End Sub

A confirmation of the delete action is provided. When a user selects yes, the record is deleted in the database using the ShowYesNo method. When the record is deleted, the mode is turned to Add and the screen cleared.

B4X:
Private Sub YesNoProcess(Tag As String)
    Select Case Tag
    Case "DeleteRecord"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        Dim bDeleted As Boolean = ABMShared.SQLRecordDelete(SQL, "news", "id", ActiveID)
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
        If bDeleted = True Then
            ABMShared.LocalStorageSave(page, "action", "new")
            ABMShared.LocalStorageSave(page, "id", -1)
            Clear
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record deleted successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be deleted, please try again.", 3000)
        End If
    End Select
End Sub

UpdateRecord.png


DeleteRecord.png


CreateRecord.png


Please note: When you study the ABMGenerator code, you will notice that it's not so different from what is documented here. The nice flexibility of the ABMGenerator is that it helps you create such code which you can copy to your page. The only difference is that I have used ActionButtons, TopBar Buttons and Pages for input and not ModalSheets. The modal sheets are used for MessageBoxes in my case.
 

Mashiane

Expert
Licensed User
Longtime User
Sign Up

The sign up form follows the same approach discussed in the CRUD functionality above. A user is afforded an input screen, now with 2 columns of entry on medium and large devices. This was achieved by creating the page grid like this.

B4X:
' create the page grid
    page.AddRows(1, True, "").AddCells12(1, "")
    page.AddRows(10, True, "").AddCellsOS(2, 0, 0, 0, 12, 6, 6, "")
    page.BuildGrid 'IMPORTANT once you loaded the complete grid AND before you start adding components

This approach was picked from the ABMaterial Demo example. This creates 1 row with 1 column spanning 12 spaces each. Then 10 rows are created each with 2 columns each and each column spanning 6 spaces each.

SignUp.png


The theme that is applied to the combo box is.

B4X:
MyTheme.AddComboTheme("orangecombo")
    MyTheme.Combo("orangecombo").inputcolor = ABM.COLOR_ORANGE
    MyTheme.Combo("orangecombo").forecolor = ABM.COLOR_ORANGE
    MyTheme.Combo("orangecombo").forecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.Combo("orangecombo").inputcolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.Combo("orangecombo").focusforecolor = ABM.COLOR_ORANGE
    MyTheme.Combo("orangecombo").focusforecolorintensity = ABM.INTENSITY_LIGHTEN1

And the date pickers theme is

B4X:
MyTheme.AddDateTimePickerTheme("orange")
    MyTheme.DateTimePicker("orange").inputcolor = ABM.COLOR_ORANGE
    MyTheme.DateTimePicker("orange").pickerclockinactivehandcolorintensity = ABM.INTENSITY_NORMAL
    MyTheme.DateTimePicker("orange").inputforecolor = ABM.COLOR_ORANGE
    MyTheme.DateTimePicker("orange").inputfocusforecolor = ABM.COLOR_ORANGE
    MyTheme.DateTimePicker("orange").inputcolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.DateTimePicker("orange").inputforecolorintensity = ABM.INTENSITY_LIGHTEN1
    MyTheme.DateTimePicker("orange").inputfocusforecolorintensity = ABM.INTENSITY_LIGHTEN1

I am using an email as a primary key for this users table. This meant that before a user is registered, the provided email address should first be verified if it does not already exist. This method did that..

B4X:
Private Sub SignUpProcess As Boolean
    'define a map to hold the form contents
    Dim m As Map
    'read the file contents to a map
    m = GetContents
    'validate the form contents where required
    If Validate(m) = False Then
        Return False
    End If
    'the form contents are ok, continue with the sign up process
    'define the search criteria, fields must equal
    Dim w As Map
    Dim UserMap As Map
    Dim SQL As SQL
    Dim strID As String
    w.Initialize
    'there is an email, ensure these are not duplicated.
    Dim email As String
    email = m.get("email")
    w.put("email=", email)
    'Get connection from current pool if MySQL/MSSQL
    SQL = ABMShared.SQLGet
    UserMap = ABMShared.SQLSelectRecordWhereMap(SQL,"users", w)
    'Close the connection to the database
    ABMShared.SQLClose(SQL)
    'If UserMap IsInitialize = True, email exists
    If UserMap.IsInitialized = True Then
        ShowMsgBox("The Email that has been specified is already taken. Please provide another email.")
        Return False
    End If
    'Register new user to the database
    'Get connection from current pool if MySQL/MSSQL
    SQL = ABMShared.SQLGet
    'insert the new user
    strID = ABMShared.SQLRecordInsert(SQL, "users", m)
    If strID > 0 Then
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastgreen", "User Signed Up successfully.", 3000)
    Else
        myToastId = myToastId + 1
        page.ShowToast("toast" & myToastId, "toastred", "User could not be signed up, please try again.", 3000)
    End If
    'Close the connection to the database
    ABMShared.SQLClose(SQL)
    Return True
End Sub

As soon as a user is Registered, they are taken to the Sign In page. So far so good, now for the ABMGoogleMap functionality and nearby places..
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
I am supposed to have a gallery and within this have various photo albums that could be created by users. I decided on an approach to create a folder structure that will be like \gallery\<album_name> each time a new album is added.

Albums: This list is rendered when a user selects Albums from the main menu. The list itself is stored in a database and at the backend the contents of each album are stored in folders.

Albums.png


When each album is selected, the AddEdit screen is opened where one can update the album details or even delete it.

AlbumAddEdit.png


I added another SubActionButton here to access the album photos when this album is opened. This takes one to a page with a ABMFlexWall where the contents of the \gallery\master\*.* are read and added to the ABMFlexWall dynamically on ConnectPage.

I still however have challenges with my FlexWall as it does not display like the demo flexwall, all my photos appear one after another vertically. I'm not sure whether its because the photo sizes are not the same. :(:(:(

My not so well displaying FlexWall

AlbumViewer.png


Otherwise, selecting each of the pictures, brings in up in a materialized box, cool..

PictureSelect.png


I need some ABMFlexWall advise...:eek:

CreateUpdate Method to create and update an existing album: This also creates a new folder and or renames it if its updated.

B4X:
Private Sub CreateUpdate()
    'define a map to hold the form contents
    Dim m As Map
    'read the file contents to a map
    m = GetContents
    'validate the form contents where required
    If Validate(m) = False Then
        Return
    End If
    'the form contents are ok, continue with the save
    Dim strAction As String
    Dim strID As String
    'determine the action we have been performing
    strAction = ABMShared.LocalStorageRead(page, "action")
    strID = ABMShared.LocalStorageRead(page, "id")
    Select Case strAction
    Case "new"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        'insert the record
        strID = ABMShared.SQLRecordInsert(SQL, "albums", m)
        If strID > 0 Then
            ABMShared.LocalStorageSave(page, "action", "edit")
            ABMShared.LocalStorageSave(page, "id", strID)
            Dim gallery As String
            gallery = "gallery"
            Dim albumname As String
            albumname = m.get("albumname")
            Dim www As String = "www"
            Dim fStructure As String = www & "/" & AppName & "/" & gallery & "/" & albumname
            File.MakeDir(File.DirApp, fStructure)
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record added successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be added, please try again.", 3000)
        End If
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
    Case "edit"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        If ABMShared.SQLRecordUpdate(SQL,"albums", m, "id", strID) = True Then
            Dim albumname As String
            albumname = m.get("albumname")
            Dim gallery As String
            gallery = "gallery"
            Dim www As String = "www"
            Dim nStructure As String = www & "/" & AppName & "/" & gallery & "/" & albumname
            If OldValue.Length > 0 Then
                If OldValue.ToLowerCase <> albumname.ToLowerCase Then
                    'Define the old structure
                    Dim oStructure As String = www & "/" & AppName & "/" & gallery & "/" & OldValue
                    oStructure = File.Combine(File.DirApp, oStructure)
                    nStructure = File.Combine(File.DirApp, nStructure)
                    ABMShared.RenameFolder(oStructure,nStructure)
                Else
                    File.MakeDir(File.DirApp, nStructure)
                End If
            End If
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record updated successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be updated, please try again.", 3000)
        End If
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
    End Select
End Sub

DeleteFolder Confirmation

When a user selects to delete an album, the folder is also deleted with all the contents.

B4X:
Private Sub YesNoProcess(Tag As String)
    Select Case Tag
    Case "DeleteRecord"
        Dim SQL As SQL
        'get the database connection
        Dim SQL As SQL = ABMShared.SQLGet
        Dim bDeleted As Boolean = ABMShared.SQLRecordDelete(SQL, "albums", "id", ActiveID)
        'Close the connection to the database
        ABMShared.SQLClose(SQL)
        If bDeleted = True Then
            ABMShared.LocalStorageSave(page, "action", "new")
            ABMShared.LocalStorageSave(page, "id", -1)
            'define a map to hold the form contents
            Dim m As Map
            'read the record contents to a map
            m = GetContents
            Dim gallery As String
            gallery = "gallery"
            Dim albumname As String
            albumname = m.get("albumname")
            Dim www As String = "www"
            Dim fStructure As String = www & "/" & AppName & "/" & gallery & "/" & albumname
            fStructure = File.Combine(File.DirApp, fStructure)
            ABMShared.DeleteFolder(fStructure)
            Clear
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastgreen", "Record deleted successfully.", 3000)
        Else
            myToastId = myToastId + 1
            page.ShowToast("toast" & myToastId, "toastred", "Record could not be deleted, please try again.", 3000)
        End If
    End Select
End Sub

Additional ABMShared Methods

B4X:
Public Sub RenameFolder(OldName As String, NewName As String)
    Dim jo1, jo2 As JavaObject
    jo1.InitializeNewInstance("java.io.File", Array(OldName))
    jo2.InitializeNewInstance("java.io.File", Array(NewName))
    Return jo1.RunMethod("renameTo", Array(jo2))
End Sub
Public Sub DeleteFolder(Folder As String)
    Try
        For Each f As String In File.ListFiles(Folder)
            If File.Exists(Folder,f) = True Then File.Delete(Folder, f)
        Next
        File.Delete("",Folder)
    Catch
    End Try
End Sub

I will explore adding photos to using the uploader soon. The RenameFolder method was found somewhere here in the forum.

This is such an amazing framework and using the server functions with it is just amazing, offcourse after I remove some of my confusion about things... ;)
 
Last edited:

Mashiane

Expert
Licensed User
Longtime User
That's not a secret :) I mention on every page in the demo in the footer ABMaterial started with Materialize CSS. However, since then, ABMaterial has drifted a long way from it and is not compatible with their current version any more. But it was a good start!
It was hey, you are brilliant. I guess I missed that part due to excitement. lol. Just noticed they have a character counter for their input controls and some cool additions. This behind the scenes look is helping to understand your framework a great deal. Thanks for this wonder! All the best.
 
Top