Showing posts with label database. Show all posts
Showing posts with label database. Show all posts

Saturday, January 31, 2015

KirSQLite Flex AIR Database Manager Part 2

In this tutorial well work a little more on the layout - the data grid of the table view screen, specifically.

Firstly, well turn the contents of the first NavigatorContent object into a VGroup, which contains an HBox (something I added just now) and an AdvancedDataGrid that we already have.

The HBox is a container which will contain some tools for editing the data grid. Well add a new column to the datagrid today, which will be a column of CheckBox objects. This way the user will be able to select one or multiple items to perform an action on all of them, for example, delete them all.

For this feature specifically the HBox will contain a CheckBox which will allow the user to select or deselect all of the entries in the table. Set its label to "Select all" and set its change event listener to a function called selectAllChange(), and pass "event" in the parameter. Next to the checkbox, add a button (which does nothing right now) with a label "Delete selected".

<mx:HBox width="100%" height="30" paddingLeft="8" paddingTop="6">
<mx:CheckBox label="Select all" change="selectAllChange(event);" />
<s:Button label="Delete selected" />
</mx:HBox>

In the AdvancedDataGrid itself, well do quite a few changes too. First, set its id to tableGrid. Set its editable property to true.

In the columns array, add a new AdvancedDataGridColumn object with blank headerText and width of 30. Set its sortable, draggable, editable and resizable properties to false. Set this particular columns itemRenderer to an fx:Component container, which contains an mx:Box container, which contains a CheckBox object. Set this CheckBoxs selected property bound to data.sel.

Set the idnum columns editable property to false. Those are all the changes we made to the data grid:

<mx:AdvancedDataGrid id="tableGrid" width="100%" height="100%" dataProvider="{testData}" editable="true">
<mx:columns>
<mx:AdvancedDataGridColumn headerText=" " width="30" sortable="false" draggable="false" resizable="false" editable="false">
<mx:itemRenderer>
<fx:Component>
<mx:Box width="30" horizontalAlign="center">
<mx:CheckBox selected="@{data.sel}" />
</mx:Box>
</fx:Component>
</mx:itemRenderer>
</mx:AdvancedDataGridColumn>
<mx:AdvancedDataGridColumn dataField="idnum" headerText="ID" editable="false" />
<mx:AdvancedDataGridColumn dataField="firstname" headerText="First name" />
<mx:AdvancedDataGridColumn dataField="lastname" headerText="Last name" />
<mx:AdvancedDataGridColumn dataField="age" headerText="Age"/>
</mx:columns>
</mx:AdvancedDataGrid>

The "sel" property of the "data" object is a boolean variable that is set in the testData array collection. Lets add that to all the items in the array and set the values to false. Lets also increase the number of objects in the array for testing purposes:

<mx:ArrayCollection id="testData">
<fx:Object idnum="1" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="2" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="3" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="4" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="5" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="6" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="7" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="8" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="9" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="10" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="11" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="12" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="13" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="14" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="15" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="16" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="17" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="18" sel="false" firstname="Bob" lastname="Smith" age="33" />
</mx:ArrayCollection>

Now create fx:Script tags to create a function called selectAllChange(). This function checks if the "Select all" checkbox is selected or not. If it is selected, then loop through all items in testData and set their "sel" properties to true. Otherwise, loop through them and set the values to false.

<fx:Script>
<![CDATA[
import flash.events.Event;
import mx.controls.Alert;

private function selectAllChange(evt:Event):void {
var i:int;
if (evt.currentTarget.selected) {
for (i = 0; i < testData.length; i++) {
testData[i].sel = true;
}
} else
if (!evt.currentTarget.selected) {
for (i = 0; i < testData.length; i++) {
testData[i].sel = false;
}
}
tableGrid.invalidateDisplayList();
tableGrid.invalidateList();
}
]]>
</fx:Script>

Whats worth noticing is that all List-like components in Flex (such as List, DataGrid and TileList) dont always create a separate item renderer for each item in the data provider. For example, if we have 500 items in the List, it doesnt mean that the List will have 500 item renderers that all display values for their own assigned items. In these situations, the component usually has a scrollbar (or even two) and not all of the items are visible. For example, only 10 items might be visible out of 500 at a time. The List component just displays 10 and changes their values as the user scrolls through the content.

This boosts up the performance a lot, but can also cause some problems. Like in our situation - if we have a lot of items in the datagrid, say, 100, we obviously wont be able to view all of them at the same time, maybe around 15 at a time - the data grid will only display these 15 visible items.

Whats the problem then? Its that if we use custom modifications, such as CheckBoxes as item renderers of the columns, they also keep their properties like "selected" even if the item renderer is switched to display another item, which might have a different value.

To prevent this, we must bind the select value to data.sel using the {} braces. Because our check boxes are also editable (the user can tick and untick them), we two-way-bind them using @{data.sel}.

Full code so far:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" showStatusBar="false">

<s:menu>
<mx:FlexNativeMenu dataProvider="{windowMenu}" showRoot="false" labelField="@label" keyEquivalentField="@key"/>
</s:menu>

<fx:Declarations>
<fx:XML id="windowMenu">
<root>
<menuitem label="Database">
<menuitem label="New" key="n" controlKey="true" />
<menuitem label="Open" key="o" controlKey="true" />
<menuitem label="Save" key="s" controlKey="true" />
<menuitem label="Save As.." key="s" controlKey="true" shiftKey="true" />
</menuitem>
<menuitem label="Table">
<menuitem label="New" key="t" controlKey="true" />
<menuitem label="Edit" key="e" controlKey="true" />
<menuitem label="Delete"/>
</menuitem>
</root>
</fx:XML>
<fx:XMLList id="testTree">
<root>
<treeitem label="Database1">
<treeitem label="Table1"/>
<treeitem label="Table2"/>
<treeitem label="Table3"/>
</treeitem>
</root>
</fx:XMLList>
<mx:ArrayCollection id="testData">
<fx:Object idnum="1" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="2" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="3" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="4" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="5" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="6" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="7" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="8" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="9" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="10" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="11" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="12" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="13" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="14" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="15" sel="false" firstname="Bob" lastname="Smith" age="33" />
<fx:Object idnum="16" sel="false" firstname="John" lastname="Jackson" age="32" />
<fx:Object idnum="17" sel="false" firstname="Ted" lastname="Jacobson" age="25" />
<fx:Object idnum="18" sel="false" firstname="Bob" lastname="Smith" age="33" />
</mx:ArrayCollection>
</fx:Declarations>

<fx:Script>
<![CDATA[
import flash.events.Event;
import mx.controls.Alert;

private function selectAllChange(evt:Event):void {
var i:int;
if (evt.currentTarget.selected) {
for (i = 0; i < testData.length; i++) {
testData[i].sel = true;
}
} else
if (!evt.currentTarget.selected) {
for (i = 0; i < testData.length; i++) {
testData[i].sel = false;
}
}
tableGrid.invalidateDisplayList();
tableGrid.invalidateList();
}
]]>
</fx:Script>

<s:HGroup gap="0" width="100%" height="100%">
<s:VGroup width="200" height="100%" gap="0">
<mx:Tree width="100%" height="100%" dataProvider="{testTree}" showRoot="false" labelField="@label"/>
</s:VGroup>
<s:VGroup width="100%" height="100%" gap="0">
<mx:TabNavigator width="100%" height="100%" paddingTop="0">
<s:NavigatorContent label="Table contents">
<s:VGroup width="100%" height="100%" gap="0">
<mx:HBox width="100%" height="30" paddingLeft="8" paddingTop="6">
<mx:CheckBox label="Select all" change="selectAllChange(event);" />
<s:Button label="Delete selected" />
</mx:HBox>
<mx:AdvancedDataGrid id="tableGrid" width="100%" height="100%" dataProvider="{testData}" editable="true">
<mx:columns>
<mx:AdvancedDataGridColumn headerText=" " width="30" sortable="false" draggable="false" resizable="false" editable="false">
<mx:itemRenderer>
<fx:Component>
<mx:Box width="30" horizontalAlign="center">
<mx:CheckBox selected="@{data.sel}" />
</mx:Box>
</fx:Component>
</mx:itemRenderer>
</mx:AdvancedDataGridColumn>
<mx:AdvancedDataGridColumn dataField="idnum" headerText="ID" editable="false" />
<mx:AdvancedDataGridColumn dataField="firstname" headerText="First name" />
<mx:AdvancedDataGridColumn dataField="lastname" headerText="Last name" />
<mx:AdvancedDataGridColumn dataField="age" headerText="Age"/>
</mx:columns>
</mx:AdvancedDataGrid>
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent label="Edit columns">

</s:NavigatorContent>
<s:NavigatorContent label="Query">

</s:NavigatorContent>
</mx:TabNavigator>
</s:VGroup>
</s:HGroup>

</s:WindowedApplication>

Thanks for reading!
Read more »

Wednesday, January 28, 2015

KirSQLite Flex AIR Database Manager Part 5

In this part of the tutorial well begin to read the structure of the opened SQLite database.

First of all, lets optimize the code we have right now. We can do that by putting some of the code from openDatabase() and newDatabase() functions into a separate function called parseDatabase(), which receives 2 parameters - the File object of the database and a boolean needCheck. The function basically opens the database and parses it. If needCheck is set to true, it also checks if this database exists before continuing.

private function parseDatabase(file:File, needCheck:Boolean = false):String {
var ret:String = "";
if (!needCheck || file.exists) {
if(!needCheck || notAlreadyOpen(file)){
var newDB:XML;
if (dbData.db.length() == 0) {
connection.open(file);
dbData = new XMLList(<root></root>);
newDB = <db/>
newDB.@label = file.name;
newDB.@numid = 1;
newDB.@isBranch = true;
newDB.@path = file.nativePath;
dbData[0].appendChild(newDB);
ret = "main";
}else
if (dbData.db.length() > 0) {
var newnum:int = dbData.db.length() + 1;
connection.attach("db"+newnum.toString(), file);
newDB = <db/>
newDB.@label = file.name;
newDB.@numid = newnum.toString();
newDB.@isBranch = true;
newDB.@path = file.nativePath;
dbData[0].appendChild(newDB);
ret = "db" + newnum.toString();
}}else {
Alert.show("Database already opened.", "Error");
}
}else {
Alert.show("File not found.", "Error");
}
return ret;
}

Now we can replace those repeating lines in newDatabase() and openDatabase() by this function.

You might have noticed that the function returns a string value. This value is the name of the newly created database in our connection object. We use this value to pass as a parameter when we call loadDataSchema() function - a function that is called right after parseDatabase() to load and parse the content of the database.

private function newDatabase():void {
var file:File = File.desktopDirectory.resolvePath("Untitled");
file.addEventListener(Event.SELECT, newSelect);
file.browseForSave("Choose where to save the database");
var newDB:XML;
var statement:SQLStatement = new SQLStatement();
function newSelect(evt:Event):void {
file.nativePath += ".db";
var n:String = parseDatabase(file);
loadDataSchema(n);
}
}

private function openDatabase():void {
var file:File = new File();
file.browseForOpen("Open database", [new FileFilter("Databases", "*.db"), new FileFilter("All files", "*")]);
file.addEventListener(Event.SELECT, openSelect);

function openSelect(evt:Event):void {
var n:String = parseDatabase(file, true);
loadDataSchema(n);
}
}

This is the part where it got tricky for me. To find out information about inner content of the databases such as tables and indices, we need to load a schema. In AIR this is done using loadSchema() method. However, the schema does not exist if the databse is empty. That is why we first load the schema to add 2 event listeners - one function is called if the operation was successful (the schema exists), and the other one will be called if there is an error (the schema does not exist).

We pass a bunch of parameters to the loadSchema() method, including the name of the database were loading and a Responder object containing references to success and error functions.

In the success function, well load the schema one more time, then read it using getSchemResult() method, and display the number of tables in the database. The schema needs to be loaded again before we read the results.

private function loadDataSchema(name:String):void {
if (name != "") {
connection.loadSchema(null, null, name, true, new Responder(schemaSuccess, schemaError));
function schemaSuccess(evt:SQLSchemaResult):void {
// Schema found! Now parsing:
connection.loadSchema(null, null, name, true);
var result:SQLSchemaResult = connection.getSchemaResult();
Alert.show("Schema found! Tables in this database: " + result.tables.length);
}
function schemaError(evt:SQLError):void {
Alert.show("Database is empty");
}
}
}

Now we can open databases and view how many tables it contains.

Full code:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" showStatusBar="false">

<s:menu>
<mx:FlexNativeMenu dataProvider="{windowMenu}" showRoot="false" labelField="@label" keyEquivalentField="@key" itemClick="menuSelect(event);" />
</s:menu>

<fx:Declarations>
<fx:XML id="windowMenu">
<root>
<menuitem label="Database">
<menuitem id="newdb" label="New" key="n" controlKey="true" />
<menuitem id="opendb" label="Open" key="o" controlKey="true" />
<menuitem label="Save" key="s" controlKey="true" />
<menuitem label="Save As.." key="s" controlKey="true" shiftKey="true" />
</menuitem>
<menuitem label="Table">
<menuitem label="Add" key="t" controlKey="true" />
<menuitem label="Edit" key="e" controlKey="true" />
<menuitem label="Delete"/>
</menuitem>
</root>
</fx:XML>
<fx:XMLList id="dbData">
</fx:XMLList>
<mx:ArrayCollection id="tableData">
</mx:ArrayCollection>
</fx:Declarations>

<fx:Script>
<![CDATA[
import flash.data.SQLConnection;
import flash.data.SQLResult;
import flash.data.SQLSchema;
import flash.data.SQLSchemaResult;
import flash.data.SQLStatement;
import flash.errors.SQLError;
import flash.events.Event;
import flash.events.SQLEvent;
import flash.filesystem.File;
import flash.net.FileFilter;
import flash.net.Responder;
import mx.controls.Alert;
import mx.events.FlexNativeMenuEvent;

private var connection:SQLConnection = new SQLConnection();

private function selectAllChange(evt:Event):void {
var i:int;
if (evt.currentTarget.selected) {
for (i = 0; i < tableData.length; i++) {
tableData[i].sel = true;
}
} else
if (!evt.currentTarget.selected) {
for (i = 0; i < tableData.length; i++) {
tableData[i].sel = false;
}
}
tableGrid.invalidateDisplayList();
tableGrid.invalidateList();
}

private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@id == "newdb")?(newDatabase()):(void);
(evt.item.@id == "opendb")?(openDatabase()):(void);
}

private function newDatabase():void {
var file:File = File.desktopDirectory.resolvePath("Untitled");
file.addEventListener(Event.SELECT, newSelect);
file.browseForSave("Choose where to save the database");
var newDB:XML;
var statement:SQLStatement = new SQLStatement();
function newSelect(evt:Event):void {
file.nativePath += ".db";
var n:String = parseDatabase(file);
loadDataSchema(n);
}
}

private function openDatabase():void {
var file:File = new File();
file.browseForOpen("Open database", [new FileFilter("Databases", "*.db"), new FileFilter("All files", "*")]);
file.addEventListener(Event.SELECT, openSelect);

function openSelect(evt:Event):void {
var n:String = parseDatabase(file, true);
loadDataSchema(n);
}
}

private function loadDataSchema(name:String):void {
if (name != "") {
connection.loadSchema(null, null, name, true, new Responder(schemaSuccess, schemaError));
function schemaSuccess(evt:SQLSchemaResult):void {
// Schema found! Now parsing:
connection.loadSchema(null, null, name, true);
var result:SQLSchemaResult = connection.getSchemaResult();
Alert.show("Schema found! Tables in this database: " + result.tables.length);
}
function schemaError(evt:SQLError):void {
Alert.show("Database is empty");
}
}
}

private function parseDatabase(file:File, needCheck:Boolean = false):String {
var ret:String = "";
if (!needCheck || file.exists) {
if(!needCheck || notAlreadyOpen(file)){
var newDB:XML;
if (dbData.db.length() == 0) {
connection.open(file);
dbData = new XMLList(<root></root>);
newDB = <db/>
newDB.@label = file.name;
newDB.@numid = 1;
newDB.@isBranch = true;
newDB.@path = file.nativePath;
dbData[0].appendChild(newDB);
ret = "main";
}else
if (dbData.db.length() > 0) {
var newnum:int = dbData.db.length() + 1;
connection.attach("db"+newnum.toString(), file);
newDB = <db/>
newDB.@label = file.name;
newDB.@numid = newnum.toString();
newDB.@isBranch = true;
newDB.@path = file.nativePath;
dbData[0].appendChild(newDB);
ret = "db" + newnum.toString();
}}else {
Alert.show("Database already opened.", "Error");
}
}else {
Alert.show("File not found.", "Error");
}
return ret;
}

private function notAlreadyOpen(file:File):Boolean{
var r:Boolean = true;
for (var i:int = 0; i < dbData.db.length(); i++) {
if (file.nativePath == dbData.db[i].@path) {
r = false;
}
}
return r;
}
]]>
</fx:Script>

<s:HGroup gap="0" width="100%" height="100%">
<s:VGroup width="200" height="100%" gap="0">
<mx:Tree width="100%" height="100%" dataProvider="{dbData}" showRoot="false" labelField="@label"/>
</s:VGroup>
<s:VGroup width="100%" height="100%" gap="0">
<mx:TabNavigator width="100%" height="100%" paddingTop="0">
<s:NavigatorContent label="Table contents">
<s:VGroup width="100%" height="100%" gap="0">
<mx:HBox width="100%" height="30" paddingLeft="8" paddingTop="6">
<mx:CheckBox label="Select all" change="selectAllChange(event);" />
<s:Button label="Delete selected"/>
</mx:HBox>
<mx:AdvancedDataGrid id="tableGrid" width="100%" height="100%" dataProvider="{tableData}" editable="true">
<mx:columns>
<mx:AdvancedDataGridColumn headerText=" " width="30" sortable="false" draggable="false" resizable="false" editable="false">
<mx:itemRenderer>
<fx:Component>
<mx:Box width="30" horizontalAlign="center">
<mx:CheckBox selected="@{data.sel}" />
</mx:Box>
</fx:Component>
</mx:itemRenderer>
</mx:AdvancedDataGridColumn>
<mx:AdvancedDataGridColumn dataField="idnum" headerText="ID" editable="false" />
<mx:AdvancedDataGridColumn dataField="firstname" headerText="First name" />
<mx:AdvancedDataGridColumn dataField="lastname" headerText="Last name" />
<mx:AdvancedDataGridColumn dataField="age" headerText="Age"/>
</mx:columns>
</mx:AdvancedDataGrid>
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent label="Edit columns">

</s:NavigatorContent>
<s:NavigatorContent label="Query">

</s:NavigatorContent>
</mx:TabNavigator>
</s:VGroup>
</s:HGroup>

</s:WindowedApplication>

Thanks for reading!
Read more »