» » Sciter UI, application architecture

Sciter UI, application architecture

Architecture of applications that use Sciter based UI can be visualized as this:

[画像:Application layers]

Application layers

Typically such applications contain two distinct layers:

  • UI layer that uses Sciter window with loaded HTML/CSS and scripts (code behind UI);
  • Application logic layer, most of the time that is native code implementing logic of the application.

Ideally these two layers shall be split appart – isolated from each other as they use conceptually different code models and probably code styles.

UI layer uses event driven model: “on click here expand section there and send request to logic layer for some data”.

Application logic layer (ALL) is more linear usually. It is is a collection of functions that accepts some parameters and return some data. Even if ALL uses threads, code inside such threads is still linear.

UI and app-logic interaction principles:

Most of the time code execution in UI applications is initiated by the UI itself but sometimes application code may generate its own events. For the UI such events are not anyhow different from pure UI events like mouse/keyboard clicks and the like. Informational flow between UI and ALL conceptually falls into these three groups:

  1. “get-request” – synchronous calls from UI to logic layer to get some data:
  2. “post-request” – asynchronous calls with callback “when-ready” functions:
  3. “application events” – application detects some change and needs to notify UI to reflect it somehow:

To support all these scenarios application can use only two “entry points” :

  • UI-to-logic calls: native properties and methods defined on event_handler derived class. These methods must be advertised in SOM_PASSPORT_BEGIN/END block in order to be accessible from script side.
  • logic-to-UI calls: sciter::host:call_function(name, args ) – calls scripting function from C/C++ code. The name here could be a path: “namespace.func”.
  • UI-to-logic-with-callbacks calls. Native methods can be designed to receive callbacks – functions defined in script and passed to native functions to be called on some events: view.AppWindow.nativeThreadStart( function(result) {...});

Calling native code from UI

To handle UI-to-logic calls the application defines class derived from sciter::event_handler and attaches its instance to the Sciter window (view). C++ methods defined on the class can be advertised to be used on script side:

 class AppWindow
 : public sciter::host<AppWindow>
 , public sciter::event_handler
 {
 HWND _hwnd;
 ...
 
 int getSomeData(int param1, sciter::string param2); 
 SOM_PASSPORT_BEGIN(AppWindow)
 SOM_FUNCS(
 SOM_FUNC(getSomeData)
 )
 SOM_PASSPORT_END
 }

That can be used in script as:

var data = view.AppWindow.getSomeData(param1, param2);

that will end up in this C/C++ call:

int getSomeData(int param1, sciter::string param2) {...}

Check Sciter Object Model for how to advertise native classes and functions using SOM_PASSPORT.

Application events

Application can generate some events by itself. When some condition or state inside application changes it may want to notify the UI about it. To do that application code can simply call function in script namespace with needed parameters.

Let’s assume that script has following declaration:

namespace Accounts 
{
 function created( accountId, accountProps ) {
 $(#accountList).append(...);
 }
 function deleted( accountId, accountProps ) {
 $(#accountList li[accid={accountId}]).remove();
 }
}

Then the application code can fire such events by simply calling:

window* pw = ...
pw->call_function("Accounts.created", accId, accFields );
pw->call_function("Accounts.deleted", accId );

Script functions as callbacks for native code

Need of callbacks arises when some of work needs to be done inside worker threads. Some task either take too long to complete or data for them needs to be loaded from the Net or other remote sources. UI cannot be blocked for long time – it still shall operate and be responsive. The same situation happens in Web applications when JavaScript needs to send AJAX request. In this case callback functions are used. Call to native code includes reference to script function that will be executed when the requested data is available.

Consider this UI script function that asks app-logic to create some account on a remote server:

function createAccount( accountProps ) 
 {
 function whenCreated( accountId ) // inner callback function
 { 
 $(#accountList).append(...);
 }
 view.AppWindow.createAccount(accountProps, whenCreated );
 }

It passes accountProps data and callback function reference to the createAccount() method that may run a thread. This thread creates the account (presumably takes some time) and invokes whenCreated at the end.

void add_account_thread(sciter::value props, sciter::value whenCreated) 
{
 // the thread body
 // ... do some time consuming stuff ...
 sciter::value accountId = createAccount(props);
 // Sciter API is thread safe so we can call 
 // the callback directly from worker thread:
 whenCreated.call(accountId); 
}
bool createAccount(sciter::value props, sciter::value whenCreated) {
 std::thread t(add_account_thread,props,whenCreated); 
 t.detach(); 
 return true;
}

Note that whenCreated.call(accountId); will block current worker thread until UI will not execute that call.

Global custom native namespaces, functions and objects

If needed, application can provide set of native functions and variables to be accessible from script.

To do that you will need

  1. to declare a class derived from sciter::om::asset base;
  2. to advertise methods and properties of that class by listing them in SOM_PASSPORT_BEGIN/END block;
  3. to call SciterSetGlobalAsset(asset);

Here is an example of SQLite native library added to a global namespace of script environment:

int uimain(std::function<int()> run ) {
 ...
 
 SciterSetGlobalAsset(new sqlite::SQLite()); // adding SQLite as a global namespace object
}

The code above is from usciter.exe demo project.

Where sqlite::SQLite is declared as:

// namespace object: 
 class SQLite : public sciter::om::asset {
 public:
 sciter::string version = WSTR(SQLITE_VERSION);
 SQLite() {}
 sciter::value open(sciter::string path) {
 sqlite::DB* pdb = DB::open(path); // creating new native DB instance 
 if (pdb)
 return sciter::value::wrap_asset(pdb); // returning it to script
 else
 return sciter::value();
 }
 SOM_PASSPORT_BEGIN(SQLite)
 SOM_PASSPORT_FLAGS(SOM_EXTENDABLE_OBJECT) // allow SQLite to be extended as a namespace
 SOM_FUNCS(SOM_FUNC(open))
 SOM_PROPS(SOM_RO_PROP(version))
 SOM_PASSPORT_END
 };

See full source of SQLite namespace.

Epilogue

Having just two “ports” (out-bound UI-to-logic and in-bound logic-to-UI) is a good thing in principle. This allows to isolate effectively two different worlds – asynchronous UI and deterministic application logic world. Easily “debuggable” and manageable.

HTML, CSS and script (code behind UI) runs in most natural mode and application core is comfortable too – not tied with the UI and its event and threading model.

AltStyle によって変換されたページ (->オリジナル) /