Building native *nix solutions with C++, GTK+ and SQL Server vNext

Hi Community,

This is the fourth and final post of this series on GTK+ and Linux development with C++ and SQL Server vNext.

The purpose of this post is to walk you through a demo application I’ve written using a few different technologies, tools and patterns as listed below:

My language of choice and the one used for this application is C++, reason being is that I could easily compile this solution for any other platform and it will just work; but C++  is also a modern and elegant language that allows me to do anything I want.

Another point I’d like to make is that patterns are language-agnostic, languages the way I see them are just syntactic sugar that allow developers to express themselves. In my personal case, English and “Venezolano” (Spanish) enable me to express differently (mainly because of the cultural differences)  but the message remains the same.

I think that the best way to get started is by setting up SQL Server vNext on an Ubuntu VM and then describe the bits and pieces of the demo application. My main operating system is Linux (I think that’s one of the reasons why I’m not an MVP any longer) because it just simply works and performs heaps better than other OSes (most supercomputers runs Linux, for instance).

The animated GIF below depicts the application in operation performing CRUD functions.

Most applications regardless of the technology and framework used to build them provide a DataContext (or BindingSource). Its purpose is to store a local offline copy of the required data retrieved from a backend system (e.g.: RDBMS or Web Service, for instance).

Main reason for this is performance and flexibility, connection is established with backend for updates only and then all of the networking plumbing comes into play, this is by far more efficient than having a dedicated connection (socket) that might be idle most of the time, so if you’re a developer reading this, and in your code you usually keep a connection I would strongly suggest you to revise your design. In the demo application, I provide the DataContext functionality in the class with the same name.

////////////////////////////////////////////

// DataContext class (Definition)

///////////////////////////////////////////


#ifndef SQLTESTHARNESS_DATACONTEXT_H

#define SQLTESTHARNESS_DATACONTEXT_H


#include "CommonHeaders.h"

#include "ContactModel.h"

#include "SqlDal.h"



class DataContext {

private:

    bool canEdit;

    int RowPosition = 0;

    std::vector<Contact> Rows;

    ParentStub<Contact> Parent;

    static GtkApplication *application;

    Contact CurrentContact, PreviousContact;


public:

    DataContext();

    SqlDal DataAccess;

    int RowCount_get();

    bool CanEdit_get();

    void FetchDataSet();

    int RowPosition_get();

    void CanEdit_set(bool value);

    void RowPosition_set(int index);

    std::vector<Contact>& Rows_get();

    Contact CurrentContact_get() const;

    Contact PreviousContact_get() const;

    void PreviousContact_set(Contact contact);

    void Rows_set(std::vector<Contact> rows);

    DataContext(GtkApplication& app, ParentStub<Contact> parent);

    void GetOrSetValuesInUiThroughCallback(UiFieldOperation op, Contact& contact);

    void UpdateContextPostDbOperation(const Contact& contact, DatabaseOperation operation);

    void CurrentContact_set(Contact contact, std::function<void(Contact&)> updateUiCallback = nullptr);

    void Navigate(NavigationDirection direction, bool calledFromGrid = false, std::function<void(void)> removeItemOnDelete = nullptr);

};



#endif //SQLTESTHARNESS_DATACONTEXT_H


////////////////////////////////////////////

// DataContext class (Implementation)

///////////////////////////////////////////



#include "DataContext.h"


GtkApplication*::DataContext::application;


/*

 *

 */

int DataContext::RowCount_get() {

    return (int) Rows.size();

}


/*

 *

 */

void DataContext::Navigate(NavigationDirection direction, bool calledFromGrid, std::function<void(void)> removeItemOnDelete ) {

    auto rowCount = Rows.size();


    if (rowCount > 0) {

        auto currentPos = RowPosition_get();


        // This code only runs when deleting a record. Otherwise, grid databinding

        // won't reflect the changes in the UI when calling Parent.NotifyParent() below

        if (removeItemOnDelete != nullptr)

            removeItemOnDelete();


        switch(direction) {

            case NavigationDirection::First:

                RowPosition_set(0);

                break;

            case NavigationDirection::Previous:

                if (currentPos > 0)

                    RowPosition_set(currentPos - 1);

                else RowPosition_set(0);

                break;

            case NavigationDirection::Next:

                if (currentPos < RowCount_get() - 1)

                    RowPosition_set(currentPos + 1);

                break;

            case NavigationDirection::Last:

                RowPosition_set(rowCount - 1);

                break;

        }

    } else if (!calledFromGrid) {

        auto dialog = gtk_message_dialog_new(gtk_application_get_active_window (application),

                                             GTK_DIALOG_MODAL, GTK_MESSAGE_INFO,

                                             GTK_BUTTONS_OK, "No records found");


      if (gtk_dialog_run((GtkDialog*) dialog) == GTK_RESPONSE_OK)

          gtk_widget_destroy(dialog);

    }

    Parent.NotifyParent(false);

}

/*

 *

 */

void DataContext::Rows_set(std::vector<Contact> rows) {

    Rows = rows;

}


/*

 *

 */

void DataContext::FetchDataSet() {

    Parent.FetchData();

}



/*

 *

 */

std::vector<Contact>& DataContext::Rows_get()  {

    return Rows;

}


void DataContext::GetOrSetValuesInUiThroughCallback(UiFieldOperation op, Contact &contact) {

    Parent.ParentUiOperation(op, contact);

}


/*

 *

 */

DataContext::DataContext() {


}


/*

 *

 */

void DataContext::CanEdit_set(bool value) {

    if (canEdit != value) {

        canEdit = value;

        Parent.EnableFieldsinParent(value);

    }

}

/*

 * 

 */

bool DataContext::CanEdit_get() {

    return canEdit;

}


/*

 *

 */

Contact DataContext::CurrentContact_get() const {

    return CurrentContact;

}


/*

 *

 */

void DataContext::CurrentContact_set(Contact contact, std::function<void(Contact&)> updateUiCallback) {

        CurrentContact = contact;


        // is it required to do anything to the UI?

        if (updateUiCallback != nullptr)

            updateUiCallback(CurrentContact);

}


/*

 *

 */

int DataContext::RowPosition_get() {

    return RowPosition;

}


/*

 *

 */

void DataContext::RowPosition_set(int index) {

    RowPosition = index;

}



/*

 *

 */

Contact DataContext::PreviousContact_get() const {

    return PreviousContact;

}


/*

 *

 */

void DataContext::PreviousContact_set(Contact contact) {

    PreviousContact = contact;

}


/*

 *

 */


void DataContext::UpdateContextPostDbOperation(const Contact& contact, DatabaseOperation operation) {

    if (!contact.IsEmpty()) {

        auto index = 0;

        auto found = std::find_if(Rows_get().begin(), Rows_get().end(), [&](Contact &c) {

            index++;

            return (c.ContactId == contact.ContactId);

        });


        if (operation == DatabaseOperation::Create) {

            Navigate(NavigationDirection::Last);

        } else if (operation == DatabaseOperation::Delete) {

            if (found != Rows_get().end())

                Navigate(NavigationDirection::Previous, true, [&]{Rows_get().erase(found);});

        } else if (operation == DatabaseOperation::Update) {

            if (found != Rows_get().end()) {

                index--;

                Rows_get().at(index).Email = contact.Email;

                Rows_get().at(index).LastName = contact.LastName;

                Rows_get().at(index).FirstName = contact.FirstName;

                Rows_get().at(index).PhoneNumber = contact.PhoneNumber;

            }

            Parent.NotifyParent(true);

        }

    }

}


/*

 *

 */

DataContext::DataContext(GtkApplication& app, ParentStub<Contact> parent) {

    application = &app;

    Parent = parent;

    DataAccess.ConnectionString_set("DSN=Contacts;UID=sa;PWD=p@ssw0rd;DATABASE=GtkDemo;");

}

Data coming from the data source is stored in a std::vector of ContactModel. Navigation is done on this vector, as well as databinding so it’s crucial to keep it in sync with backend. The ContactModel also has an indexer that allows retrieving the value of a property by its index.

Data types in use are mainly std::string. Since ODBC’s API is C-based, the ContactModel has also got a property (member) called ODBCFields that directly map to their corresponding ODBC datatypes. Changes made to the database from the application can be verified via SQuirreL as depicted below.

The database as mentioned is SQL vNext, so it’s required to download JDBC driver to connect from SQuirreL.

I find it funny and hilarious to a certain extent the fact that I can connect to SQL vNext on Linux via ODBC (even from Excel) but I cannot do it from Visual Studio.

Reason being is that ODBC implements TDS (inherited from Sybase, yes… SQL Server  “was” a Sybase product which Microsoft would commercialize for Windows and Sybase for *nix systems) whereas Visual Studio uses the ADO.NET Data provider which can connect but doesn’t recognize it as a compatible SQL Server  instance, so what’s the parable here?

Stick to standards as much as possible, trends and “fashion technologies” will always come and go over time, but standards will last much longer.As stated earlier, the application implements the following patterns, and their corresponding implementation in the demo application :

    • MVC: The model is bound to the View via the DataContext in the MainWindowController.
    • Singleton: There’s an static instance of the MainWindowController called self (equivalent of this exposed to static and external methods) which is initialized the first time the application starts, it hold references to Controls and functionality in the controller.
    • Proxy: Some functionality in the Parent or container class (MainWindowController) is passed across to children object as explained in previous post here.
    • Memento: When an edit takes place whether it’s because of a new record or making changes to an existing record, the current record is stored and it can be restored if edit is cancelled.

 

And that’s it, folks!

Please feel free to download source code from here

Regards,

Angel

Leave a Reply

Your email address will not be published. Required fields are marked *