Version 11.3 build 44452 introduces new visual theme: Harbor.
Harbor is a new PHPRunner/ASPRunner.NET theme created for modern, data-driven applications. It combines deep navy navigation, clean white surfaces, subtle borders, and a restrained blue accent. We paid special attention to the areas people use every day: grids are easier to scan, forms have more breathing room, dashboards share a consistent card style, and common actions are easier to recognize. Small touches—like removing vertical grid lines, standardizing panel headings, and adding icons to key buttons help Harbor feel cleaner and more focused while keeping the familiar workflow.
Key features:
Additional considerations:
A polished web application takes more than choosing a visual theme. Thoughtful layouts, consistent spacing, clear navigation, well-chosen icons, readable grids, and carefully organized forms all help turn a good theme into a great user experience. Here are some additional tips.
More screenshots:
Edit page:
Dashboard:
Welcome page:
Enjoy!
The World Cup 2026 Crystal Ball Challenge is over! Thank you to everyone who took part and made the challenge fun and engaging. Congratulations to all participants, and especially to the winners for their outstanding predictions!
In this tutorial we will cover all non-trivial techniques that made this project possible. As you can imagine, the main challenge making this project look sleek and easy to use and I believe we achieved this goal.
See the final leaderboard online
We will talk about:
Download project for PHPRunner
ImagePicker
ImagePicker on marketplace
This control allows a selection of a single country. We display all countries and their flags in a scrollable popup and allow to select one. Once country is selected the popup is closed.
ImageDropdown
ImageDropdown on marketplace
This is an advanced version of a regular dropdown box. We have used it to select podium teams and top scorer players. Displaying country flag next to player name definitely makes it easier to choose the player you want.
ImageChoice
ImageChoice on marketplace
This is something that resembles a regular radio-button control, it just looks nicer. We have used to allow a selection of the host team that advances further.
Each Top 8 field uses the following group settings:
$this->settings["picker\_group"] = "top8";$this->settings["max\_group\_count"] = 8;$this->settings["status\_element\_id"] = ["status\_element\_id"];$this->settings["prevent\_duplicates"] = 1;
The group name tells the controls that they belong to the same logical question. When a country is selected in one field, duplicate prevention makes it unavailable to the other seven. The shared status element (“status_element_id”) can report progress across the whole group rather than treating each field as an unrelated input. You can see status field saying something like “7 of 8 selected” at the top right corner of the first section.
The podium fields use the same idea with a different group:
$this->settings["picker\_group"] = "podium";$this->settings["prevent\_duplicates"] = 1;
Champion, second place, and third place remain separate database columns, but they behave as one coordinated set of choices. This is useful in many PHPRunner projects: room assignments, ranked preferences, product bundles, team selection, or any form where several fields use the same type of lookup wizard but must not repeat the same value.
$currentUser = Security::getUserName();$sql = DB::PrepareSQL( "SELECT p.id FROM wc\_predictions p INNER JOIN wc\_users u ON u.id = p.user\_id WHERE u.email = ':1' LIMIT 1", $currentUser);$existingId = DB::DBLookup($sql);if ($existingId) { header( "Location: wc\_predictions\_edit.php?editid1=" . rawurlencode($existingId) ); exit();}
Security::getUserName() returns the logged-in email because email is configured as the PHPRunner username field. The query translates that login identity into a prediction record ID. If the record exists, the user is redirected to Edit instead of being allowed to create another row.
The project also performs the same lookup in the Menu event. A normal user who reaches the menu is sent to Edit when a prediction exists or Add when it does not. The Menu event improves navigation; the Before Process Add event enforces the rule at the page boundary.
$lockDate = "2026-06-28 00:00:00";if (date("Y-m-d H:i:s") >= $lockDate) { $permissions = str\_replace( array("A", "E"), "", $permissions );}return $permissions;
PHPRunner represents table operations with letters in the permission string. Removing A and E disables Add and Edit while leaving allowed read operations intact. This is stronger than CSS or JavaScript because it is evaluated on the server.
The project also detects direct requests for wc_predictions_add.php or wc_predictions_edit.php and redirects them to a read-only page. That redirect is useful feedback, but the permission change is the actual security control.
The example has a hard-coded date. A reusable implementation should read the deadline and timezone from an application settings table. It should also locate the current user’s prediction ID before redirecting to View so the destination contains a valid editid1 key.
var requiredFields = [ "top8\_team1\_id", "top8\_team2\_id", "top8\_team3\_id", "top8\_team4\_id", "top8\_team5\_id", "top8\_team6\_id", "top8\_team7\_id", "top8\_team8\_id", "champion\_team\_id", "second\_place\_team\_id", "third\_place\_team\_id", "top\_scorer\_id", "final\_goes\_to\_penalties", "host\_team\_goes\_furthest\_id"];function allRequiredAnswered() { return requiredFields.every(function(fieldName) { var ctrl = Runner.getControl(pageid, fieldName); return ctrl && $.trim((ctrl.getValue() || "") + "") !== ""; });}function refreshSaveButton() { var button = pageObj.getItemButton("add\_save"); button.toggleClass("disabled", !allRequiredAnswered());}
The important detail is that the page contains both standard PHPRunner inputs and custom image controls. Listening only to ordinary change events is not enough, so the project also listens for interaction with the custom-control elements:
$(document).on( "change keyup click", "input, select, textarea, .ip-control, .idrop-control, .ichoice-card", function() { setTimeout(refreshSaveButton, 50); });
The short delay lets the custom editor update its PHPRunner control value before the page checks it. The Edit page uses the same code with the edit_save item ID. The script also adds min=”0″ and max=”10″ to the final-score inputs.
This improves the user experience, but it is not a replacement for server-side validation. Required values and numeric ranges should still be checked in Before Add and Before Edit before the record is written.
Top 8 selections are unordered. Each predicted team is tested against all eight actual Top 8 fields:
CASE WHEN p.top8\_team1\_id > 0 AND p.top8\_team1\_id IN ( NULLIF(a.top8\_team1\_id, 0), NULLIF(a.top8\_team2\_id, 0), NULLIF(a.top8\_team3\_id, 0), NULLIF(a.top8\_team4\_id, 0), NULLIF(a.top8\_team5\_id, 0), NULLIF(a.top8\_team6\_id, 0), NULLIF(a.top8\_team7\_id, 0), NULLIF(a.top8\_team8\_id, 0) ) THEN 1 ELSE 0END
NULLIF(…, 0) prevents an unanswered actual-result field stored as zero from being treated as a legitimate team. The pattern is repeated for all eight predicted positions and then multiplied by five for the point score.
The exact-position categories use direct comparisons with their own weights:
CASE WHEN a.champion\_team\_id > 0 AND p.champion\_team\_id = a.champion\_team\_id THEN 25 ELSE 0END+CASE WHEN a.second\_place\_team\_id > 0 AND p.second\_place\_team\_id = a.second\_place\_team\_id THEN 15 ELSE 0END+CASE WHEN a.third\_place\_team\_id > 0 AND p.third\_place\_team\_id = a.third\_place\_team\_id THEN 10 ELSE 0END
The remaining categories add points for top scorer, both final-score values, penalties, and the host nation progressing furthest. The result is exposed as normal Leaderboard fields, allowing PHPRunner to build a List page over calculated data.
The exported query orders by points DESC. If the published rules promise additional tie breakers—correct picks and then submission time—the ORDER BY should explicitly include them:
ORDER BY points DESC, correct\_picks DESC, submitted\_at ASC
7. Adding rank graphics, progress bars, and current-user highlightingThe SQL produces leaderboard data, but the List page turns it into a competition display. Before processing the List page, an event resets the rank counter and removes any previous current-user standing from the session:
$\_SESSION["rank"] = 1;unset( $\_SESSION["leaderboard\_current\_rank"], $\_SESSION["leaderboard\_current\_points"], $\_SESSION["leaderboard\_current\_player"]);
Before each row is displayed, the project compares the row’s player with the current user’s display name. When they match, it stores the rank and points for the standing panel and highlights the row:
$userData = Security::currentUserData();if ($data["player"] == $userData["display\_name"]) { $\_SESSION["leaderboard\_current\_rank"] = $\_SESSION["rank"]; $\_SESSION["leaderboard\_current\_points"] = $data["points"]; $record["css"] = "background:#fff7dc;";}$\_SESSION["rank"]++;
Custom field formats replace raw values with visual components. Positions 1, 2, and 3 use gold, silver, and bronze images. The correct_picks value becomes both text and a progress bar:
$percent = $value * 100 / 16;$value = '<div class="wc-correct-picks"> <span class="wc-correct-picks-value">'.$value.' / 16</span> <span class="wc-progress-track"> <span class="wc-progress-fill" style="width:'.$percent.'%"></span> </span></div>';
Two Page Designer snippets complete the page. The header displays the latest actual-answer update time, and the standing snippet reads the session values to show “Your current standing: #N with N points.” This is a useful example of coordinating List events, custom field formats, session state, snippets, images, and CSS on one generated page.
For a larger application, compare users by an immutable user ID rather than display_name. Display names can change and may not be unique.
index.php is the public homepage/landing page. It explains the challenge, dates, prizes, and entry process, then links visitors to PHPRunner’s register.php and login.php. scoring.php explains the rules implemented by the Leaderboard SQL. thankyou.php provides a deliberate completion screen after the participant saves the initial prediction.
The connection back to PHPRunner happens through events and ordinary URLs. For example, the prediction After Add event sends the browser to the standalone confirmation page:
header("Location: thankyou.php");exit();
Registration uses the opposite direction: a generated PHPRunner page sends the new account into the prediction workflow after assigning its role and logging it in:
// Before Register$userdata["usertype"] = "user";return true;// After RegisterSecurity::loginAs($userdata["email"]);header("Location: wc\_predictions\_add.php");exit();
The exported files also include include/wc_functions.php, which is loaded by the global initialization event. It provides shared rendering functions used by prediction View and leaderboard formats, allowing generated pages to display the same flags and labels as the custom controls.
This approach keeps authentication, permissions, and data entry inside PHPRunner while allowing marketing, rules, and confirmation pages to have purpose-built layouts. The important maintenance rule is to keep URLs, asset paths, and shared includes consistent between the standalone files and generated output.
We decided that using a new Edit control for such a simple task would be an overkill. Instead, we used a simple radio-button control for ‘Final goes to penalty?’ field and a bit of CSS and Javascript OnLoad event of Add/Edit pages. Javascript basically assigns CSS class to radio-button control and the styling itself is done in CSS.
$(function() { $('input[name^="radio\_final\_goes\_to\_penalties\_"]') .closest('.rnr-horizontal-lookup') .addClass('wc-penalty-toggle');});
Check classes that start with .wc-penalty-toggle in Custom CSS.
One more tweak, the display of correct_picks field on leaderboard page. It is implemented as ‘View as’ Custom and use some additional CSS classes. Here is the code:
$value = '<div class="wc-correct-picks"> <span class="wc-correct-picks-value">'.$value.' / 16</span> <span class="wc-progress-track"> <span class="wc-progress-fill" style="width: '. $value*100/16 .'%;"></span> </span></div>';
In this code 16 is the total number of picks and this is how we calculate the percentage of correct picks.
Use the PHPRunner custom Edit controls skill.Create a custom Edit control called CurrencyInput.Purpose:A currency-friendly numeric input that formats values instantly for display, while submitting a clean numeric value to the database.Requirements:- User can type 1234.5 and see $1,234.50.- Submitted value should be 1234.50, without currency symbol or thousand separators.- Support configurable currency\_symbol, symbol\_position, decimal\_places, thousand\_separator, decimal\_separator, allow\_negative, format\_on\_load, and format\_while\_typing.- Empty value should remain empty.- Must work with normal Add/Edit form submission.- Provide PHP, JavaScript, CSS, sample.php, and README.txt.- sample.php must contain settings code only, without PHP opening/closing tags.- Package files at the root of a ZIP archive.Enjoy!
Version 11.3 release is here!
Find trial version download links below. If you are a customer with a valid maintenance, you can download registered version 11.3 via your control panel.
You can watch this video on YouTube that explains how to use new features.
Trial version download links
AI features. Help with writing the code ( PHP, C#, Javascript, CSS, SQL ).
More info in the manual
Video on AI features from Corrie
Intellisense. Code completion in all areas where code can be edited.
More info
Save project as template
More info
Export/import of individual objects between projects. We you export a table in includes everything that is relevant: field settings, page designs, events etc.
More info
Small things. Adding search to all dialogs where more than a dozen objects is listed: list of projects, list of buttons, or code snippets etc.
New cloud providers: Google Cloud and Azure Blob Storage
Google Cloud Storage info
Azure Blob Storage info
Enjoy!
The final version of PHPRunner/ASPRunner.NET 11.2 is here! To download a registered version logon to your control panel account and find download links under ‘My purchases’.
All others can find trial version links below.
Trial version download linksPHPRunner 11.2 for Windows trial
ASPRunner.NET 11.2 for Windows trial
PHPRunner 11.2 for Mac trial
PHPRunner 11.2 for Linux (Debian) trial
PHPRunner 11.2 for Linux (RedHat) trial
You can install and run it side by side with versions 10.x and 11.1. Existing software functionality will not be affected. Just make sure you do not launch previous version and v11.2 at the same time if you use a built-in database to store projects.
This new version comes with ten improvements.
Cards layout on the List pageSimilar to vertical or columns layout but lightweight. No field names, just the data. Fixed width and height. View page of the record can be open by click on the whole card on on one of the fields. Customization: list of fields, layout, fonts, colors, paddings, borders, rounded corners.
Two new visual themesNew visual themes are named Futuro (light) and Obsidian (dark). You can choose them on the Style Editor screen. They are modern, sleek and lightweight.
Three new welcome page layoutsNew Welcome page layouts are called Nebula, Optima and Sonic. You can switch between them while on the Page Designer screen and menu page is open. Here is an example of how Nebula layout looks.
Dashboards tabsYou can combine several dashboard elements into a single one and use tabs to switch between them.
Dashboard widgetsSimilar to dashboard code snippets but easier to customize.
SMTP with OAuth supportAt this moment we support Gmail Workplace and Microsoft Exchange accounts.
“Javascript Create TinyMCE” event for TinyMCE customizationThis new event allows you to customize TinyMCE editor behavior across the whole project.
‘View as’ option Copy value to clipboardThis option can be enabled for any text field under ‘View as’ settings.
Foldable elements in dashboardsAll dashboard elements can be folded by clicking on their header.
Show details key column as a hyperlinkHyperlink points to the View page of the master and can be open in the same window, in a new window or in a popup. The same functionality for the Lookup Wizard field in view mode. On screenshot below OrderID is a foreign column in ‘Order Details’ table. It is set to open master table data in a popup.
Enjoy!
Just wanted to share a preview of a couple of new features coming in version 11.2. This update is mostly about UI improvements in the generated web applications and we’ll show you new cards layout and dashboard improvements.
More details on what is new in version 11.2.
Cards layout can be customized in the Page Designer. You can choose which fields will be displayed and what are their types: image, title, subtitle, price etc.
And here is how it looks in the Page Designer. You can see tabbed section on the left side and properties like ‘foldable’ and ‘closed initially’ on the right.
Azure Key Vault offers secure storage for sensitive information like passwords, secrets etc. If you host your PHPRunner or ASPRunner.NET application on Azure, it will makes sense to store your connection strings in Key Vault. In this article we’ll show you how this can be done.
Creating Key Vault in Azure1. Create an application in Azure. Write down tenantId, clientId and clientSecret values.
More info:
https://learn.microsoft.com/ru-ru/azure/cost-management-billing/manage/create-subscription
Go back to portal home and proceed to Key Vaults. Click ‘Create’. Select a subscription, select a Resource group ( or create a new one ). Enter Key Vault name.
Now it is the time to assign access permissions. Under the Key Vault you just created proceed to ‘Access control (IAM)’. Add -> Add Role assigment, select ‘Key Vault Certificates Officer’ using search option, click ‘Next’. Select ‘User, group, or service principal’
Click ‘Select members’ search for your application name and select it. . в поисковой строке справа найти свое приложение (не пользолвателя). Next, Review + assign.
Permissions may take a bit of time to be applied but in our situation it worked right away.
Now we are ready to use this in our code.
Using Key Vault in PHPRunner1. Under Style Editor -> Custom Files add a new file named keyvault.php. Paste the code below and use your own values of tenantId, clientId and clientSecret.
```
'client\_credentials','client\_id' => $clientId,'client\_secret' => $clientSecret,'resource' => 'https://vault.azure.net');$headers = array("Content-Type"=>"application/x-www-form-urlencoded");$response = runner\_post\_request($url, $parameters, $headers);$json = runner\_json\_decode($response["content"]);if (!isset($json['access\_token'])) {throw new Exception("Failed to get access token: " . $response);}$accessToken = $json['access\_token'];$url = $vaultUrl . "secrets/".$secretName."?api-version=7.3";$headers = array("Authorization"=>"Bearer ".$accessToken,"Content-Type"=>"application/json");$response = runner\_http\_request($url, "", "GET", $headers);$result = runner\_json\_decode($response["content"]);return $result['value'];}?>``` 2. An example of using Key Vault in your application. We will create a new Server Database Connection and will retrieve database password from Key Vault. In this example we use MySQL database.
include("keyvault.php");$host="localhost";$user="root";$pwd=getSecret("pass");$port="";$sys\_dbname="cars";
Using Key Vault in ASPRunner.NET1. Under Style Editor -> Custom Files add a new file named keyvault.cs. Paste the code below and use your own values of tenantId, clientId and clientSecret.
using System;using System.IO;using System.Collections.Generic;using System.Linq;using System.Text;using System.Web;using System.Web.Mvc;using System.Reflection;using runnerDotNet;namespace runnerDotNet{public partial class CommonFunctions{public static XVar getSecret(dynamic secretName){dynamic accessToken = null, clientId = null, clientSecret = null, headers = null, json = XVar.Array(), parameters = null, result = XVar.Array(), tenantId = null, url = null, var\_response = XVar.Array(), vaultUrl = null;tenantId = new XVar("...");clientId = new XVar("...");clientSecret = new XVar("...");vaultUrl = new XVar("https://xkvault.vault.azure.net/");url = XVar.Clone(MVCFunctions.Concat("https://login.microsoftonline.com/", tenantId, "/oauth2/token"));parameters = XVar.Clone(new XVar("grant\_type", "client\_credentials", "client\_id", clientId, "client\_secret", clientSecret, "resource", "https://vault.azure.net"));headers = XVar.Clone(new XVar("Content-Type", "application/x-www-form-urlencoded"));var\_response = XVar.Clone(MVCFunctions.runner\_post\_request((XVar)(url), (XVar)(parameters), (XVar)(headers)));json = XVar.Clone(CommonFunctions.runner\_json\_decode((XVar)(var\_response["content"])));if(XVar.Pack(!(XVar)(json.KeyExists("access\_token")))){new Exception((XVar)(MVCFunctions.Concat("Failed to get access token: ", var\_response)));}accessToken = XVar.Clone(json["access\_token"]);url = XVar.Clone(MVCFunctions.Concat(vaultUrl, "secrets/", secretName, "?api-version=7.3"));headers = XVar.Clone(new XVar("Authorization", MVCFunctions.Concat("Bearer ", accessToken), "Content-Type", "application/json"));var\_response = XVar.Clone(MVCFunctions.runner\_http\_request((XVar)(url), new XVar(""), new XVar("GET"), (XVar)(headers)));result = XVar.Clone(CommonFunctions.runner\_json\_decode((XVar)(var\_response["content"])));return result["value"];}}}
2. In BeforeConnect event use the following code:
dynamic pass = CommonFunctions.getSecret("pass");GlobalVars.ConnectionStrings["conn"] = MVCFunctions.Concat("Server=localhost;Database=cars;User Id=root;Password=",pass);
In this article we will discuss how to minimize the amount of code that handles Javascript on Add/Edit pages. Making your code data-driven will help you easily manage forms with a huge numbers of fields. We will be taking care of functionality like showing/hiding fields, making fields readonly, required, disabled etc.
This approach will involve the following steps
1. Creating and populating database tables to store triggers ( when to apply the logic ) and actions ( what happens when trigger goes off )
2. Server-side PHP and C# code that passes this data to Javascript
3. Javascript code itself that listens to “change” event and implements the logic defined in the database.
Database tablesTriggers table. The structure is fairly simple. You can see that we store table name, page type, field name and what event we are listening to. In this article we will only show how to implement the most common “change” event as it covers 95% of required functionality.
Actions table is a details one while triggers is a master. They are linked by trigger_id field. This table knows what condition to check and what action to perform. Lets take a look at the first row and try to decipher it.
The action is tied to trigger #1 ( change event of Country field on the Edit page of customers table). The condition is equal and the condition_value is ‘USA’. Which means that when Country field equals ‘USA’ we should proceed with our action. And the action itself is showing of the Region field.
And here is the SQL script that will create both tables for you with sample data.
CREATE TABLE IF NOT EXISTS `actions` ( `id` int NOT NULL AUTO\_INCREMENT, `condition` varchar(250) DEFAULT NULL, `target` varchar(250) DEFAULT NULL, `action` varchar(250) DEFAULT NULL, `condition\_value` varchar(250) DEFAULT NULL, `trigger\_id` int DEFAULT NULL, KEY `Index 1` (`id`)) ENGINE=InnoDB AUTO\_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4\_0900\_ai\_ci;INSERT INTO `actions` (`id`, `condition`, `target`, `action`, `condition\_value`, `trigger\_id`) VALUES(1, 'equals', 'Region', 'show', 'USA', 1),(2, 'empty', 'ContactTitle', 'hide', NULL, 2),(3, 'notepmty', 'ContactTitle', 'readonly', NULL, 2),(4, 'notempty', 'ContactTitle', 'show', NULL, 2),(5, 'notequals', 'Region', 'hide', 'USA', 1),(6, 'notempty', 'Fax', 'disable', NULL, 2),(7, 'empty', 'Fax', 'enable', NULL, 2),(8, 'equals', 'Region', 'clear', 'USA', 1),(9, 'equals', 'Region', 'focus', 'USA', 1),(10, 'equals', 'Region', 'require', 'USA', 1);CREATE TABLE IF NOT EXISTS `triggers` ( `id` int NOT NULL AUTO\_INCREMENT, `table` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4\_unicode\_ci DEFAULT NULL, `page` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4\_unicode\_ci DEFAULT NULL, `event` varchar(50) COLLATE utf8mb4\_unicode\_ci DEFAULT NULL, `field` varchar(50) COLLATE utf8mb4\_unicode\_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO\_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4\_unicode\_ci;INSERT INTO `triggers` (`id`, `table`, `page`, `event`, `field`) VALUES(1, 'customers', 'edit', 'change', 'Country'),(2, 'customers', 'edit', 'change', 'ContactName');
Server-side codeServer-side code goes to BeforeDisplay event of each page where this functionality needs to be implemented. In our situation this code goes to BeforeDisplay event of Customers table Edit page. The code itself is fairly straightforward. It just dumps all the data from actions and triggers tables and makes this data available in Javascript via proxy object.
Note that for better code structure you need to create an external PHP or C# file, define a function there and place this code into that function. Then in all BeforeDisplay events
PHP code
$table = $pageObject->tName;$page = $pageObject->pageName;// get all the triggers for the current page and send it to Javascript$sql = DB::PrepareSQL("select * from triggers WHERE `table`=':1' and page=':2'", $table, $page);$triggers=array();$rs = DB::Query($sql);while( $data = $rs->fetchAssoc() ){ $triggers[] = $data;}// do the same for actions table$sql = DB::PrepareSQL("SELECT * FROM actions WHERE trigger\_id IN (select id from triggers WHERE `table`=':1' and page=':2'", $table, $page);$actions=array();$rs = DB::Query($sql);while( $data = $rs->fetchAssoc() ){ $actions[] = $data;}$pageObject->setProxyValue("triggers", $triggers);$pageObject->setProxyValue("actions", $actions);
C# code
dynamic actions = XVar.Array(), page = null, sql = null, triggers = XVar.Array();table = XVar.Clone(pageObject.tName);page = XVar.Clone(pageObject.pageName);sql = XVar.Clone(DB.PrepareSQL(new XVar("select * from triggers WHERE `table`=':1' and page=':2'"), (XVar)(table), (XVar)(page)));triggers = XVar.Clone(XVar.Array());rs = XVar.Clone(DB.Query((XVar)(sql)));while(XVar.Pack(data = XVar.Clone(rs.fetchAssoc()))){triggers.InitAndSetArrayItem(data, null);}sql = XVar.Clone(DB.PrepareSQL(new XVar("SELECT * FROM actions WHERE trigger\_id IN (select id from triggers WHERE `table`=':1' and page=':2'"), (XVar)(table), (XVar)(page)));actions = XVar.Clone(XVar.Array());rs = XVar.Clone(DB.Query((XVar)(sql)));while(XVar.Pack(data = XVar.Clone(rs.fetchAssoc()))){actions.InitAndSetArrayItem(data, null);}pageObject.setProxyValue(new XVar("triggers"), (XVar)(triggers));pageObject.setProxyValue(new XVar("actions"), (XVar)(actions));return null;
Javascript codeJavascript code goes to Javascript OnLoad event of the page where the action should happen, in our case this is Javascript OnLoad event of Customers table Edit page. The same idea with creating a Javascript function and calling from the external file is also valid here.
You can extend this code by adding more conditions types and more actions. Check inline comments for more info.
$('input, textarea, select, radio').on('change', function() { // 'this' refers to the element that triggered the change event const str = $(this).attr('id'); const match = str.match(/^value\_(.*?)\_/); var name; if (match) { console.log('Value changed for:', match[1] , 'New value:', $(this).val()); name = match[1]; value = $(this).val(); } // lets see if we have a trigger associated with the current field let triggers = proxy['triggers']; let actions = proxy['actions']; for (const trigger of triggers) { if (trigger["field"]==name ) { // loop through actions array to see what kind of actions we need to perform for (const action of actions) { if ( trigger["id"] == action["trigger\_id"]) { var proceed = false; // check conditions // equals if (action["condition"] == "equals") { if ( action["condition\_value"] == value ) { proceed = true; } } // not equals if (action["condition"] == "notequals") { if ( action["condition\_value"] != value ) { proceed = true; } } // empty if (action["condition"] == "empty") { if ( value.length==0 ) { proceed = true; } } // not empty if (action["condition"] == "notempty") { if ( value.length!=0 ) { proceed = true; } } // do we have mathcing conditions if (proceed) { // show action if (action["action"] == "show") { pageObj.showField(action["target"]); } // hide action if (action["action"] == "hide") { pageObj.hideField(action["target"]); } // clear action if (action["action"] == "clear") { Runner.getControl(pageid, action["target"]).setValue(); } // focus action if (action["action"] == "focus") { Runner.getControl(pageid, action["target"]).setFocus(); } // require action if (action["action"] == "require") { Runner.getControl(pageid, action["target"]).addValidation("IsRequired"); } } } } } } });
Enjoy!
Version 11.1 release is here! Is is available for both PHPRunner (Windows, Mac, Linux) and ASPRunner.NET.
Version 11 most frequently asked questions answered.
To download a registered version logon to your control panel account and find download links under ‘My purchases’.
Trial version download linksPHPRunner 11.1 for Windows trial
ASPRunner.NET 11.1 for Windows trial
PHPRunner 11.1 for Mac trial
PHPRunner 11.1 for Linux (Debian) trial
PHPRunner 11.1 for Linux (RedHat) trial
You can install and run it side by side with versions 10.x and 11.0. Existing software functionality will not be affected. Just make sure you do not launch v11 and v11.1 at the same time if you use a built-in database to store projects.
New features in v11.1* Calendar View * GANTT View * Source control systems support ( SVN, git ) * Lookup wizard enhancements
Calendar ViewWhen you launch version 11.1 you notice two new buttons on the toolbar, ‘Create Calendar’ and ‘Create GANTT chart’. Click ‘Create Calendar’. Choose to create a new table to get familiar with this feature. You can switch to using your own database table later. The software will create a new table for you and will even add some test data. You can now build the project and enjoy a fully-working Calendar page.
If you choose to use your own table make sure to proceed to ‘Calendar settings’ screen ( between Pages and Fields ) and configure your Calendar. Date field and subject field are mandatory and rest are optional.
PS. Calendar view is based on FullCalendar open source project.
GANTT ViewGANTT Chart is similar in many ways to the Calendar. We also recommend creating a new table and getting familiar with this feature first. GANTT View specific settings can be found on ‘Gantt’ screen.
You can add, edit, delete tasks, add dependent tasks, use drag-n-drop to change task/subtask dates or update task progress.
PS. We use Frappe Gantt component as a core of this new feature.
SVN/git supportSource control system support provides two main scenarios.
1. Using SVN/git for backups only.
2. Single developer working on the same project from different machines.
SVN, backup only
Install SVN command-line client software. On Windows we recommend SilkSVN.
Create a new empty folder in your SVN repository. Checkout it to any folder on your local computer.
In PHPRunner/ASPRunner.NET proceed to Project -> Version control settings and select ‘Export directory’, the same one where you performed the checkout. Other settings will be populated automatically.
‘Commit after each project saving’ – this is up to you to decide.
‘Update on each project opening’ – if you use SVN for backups only then turn this option off.
Once you performed these steps, you will see a new ‘Save and commit’ button at the bottom of the software. Use it to save your project and commit changes to repository.
SVN, working from different computers
On the first computer repeat all steps for ‘Backup only’. Enable option ‘Update on each project opening’. Now, in order to open this project you will need to use ‘Open from version control’ tab on the start screen.
On the second computer perform project checkout from SVN. On start screen proceed to ‘Open from version control’ tab, click Browse and point it to the folder where you performed the checkout. You will see a dialog with SVN settings, just leave everything as is and click ‘OK’.
Note: it is important not to use this feature for simultaneous development by multiple developers. Each commit will override changes made on another computer.
GIT
The ideology is the same but there a few extra commands you need to execute manually.
Create a new repository at github.com, for example JonDoe/Cars
Create a directory on your computer, open the terminal and change to that directory.
Run these commands, replace JonDoe/Cars with the actual repository name:
git initgit remote add origin https://github.com/JonDoe/Cars.gitgit branch -M main
4. Repeat steps 2 and 3 to access your github repository on the second computer.
Author identity unknown*** Please tell me who you are.
If this happens, run the following commands in the terminal/command line and and try saving and committing again.
git config --global user.email "you@example.com"git config --global user.name "Your Name"
6. Now you can do the same, in PHPRunner/ASPRunner.NET proceed to ‘Open from version control’ tab, click Browse and point it to the folder where you performed the checkout.
Lookup Wizard enhancementsNew feature here is ‘Edit selected’ which allows you to edit selected entry in a lookup wizard. Can be handy when you noticed a typo in Lookup Wizard data and want to fix it without leaving the page.
Enjoy!
What is Docker? Docker is a software platform that allows you to package, distribute, and run applications within self-contained units called containers. These containers include everything the application needs, like code, runtime, and libraries, ensuring consistent execution across different environments.
How this can be useful? There are many possible uses of Docker containers. You can use them for testing for instance. If you have a website that runs on PHP 8.1 and want to make sure that switching to PHP 8.3 doesn’t break anything, you can create a container based on PHP 8.3 and fully test your app there before making a switch on the main website. Other options include distributing your application to your customers as a container or using containers for web hosting if your web hosting provider supports this option.
In this article we will explain how you can package your PHPRunner application as a Docker container.
Docker Desktop installationAs a first step you need to install Docker Desktop on your local computer. Just download it from the official website and install keeping all default settings.
Once Docker Desktop is installed, launch it. Two main sections we are going to work with are Images and Containers.
A Docker image is a read-only template that contains everything needed to run an application, including the application’s code, system tools, and libraries. A Docker container is a running instance of a Docker image Think of an image as a blueprint and a container as the building constructed from that blueprint.
Project structureCreate a new folder for our Docker project i.e. C:\Project\Docker. Here is how the default Docker project looks like. src folder needs to be created manually and this is where all our PHP files will be stored. Luckily, we do not need to create the rest of file manually, Docker desktop can help us creating those.
Docker initStart by opening a terminal ( click Terminal icon in the bottom right corner ). First, switch to your project folder and then run docker init command.
cd C:\Projects\Dockerdocker init
Docker will ask you a few questions about what you want to create. Select Apache+PHP web application. You can use up and down keyboard keys to change your selection. Press Enter to make a choice.
For all other questions leave default options. Docker will create an image for you, based on your selection. On Images tab you are now going to see a new image. Docker will also tell you that now you need to run docker compose up –build command in order to build and launch your container.
DockerfileThis is how the initial dockerfile looks. I just removed some extra comments for brevity. Lets try to create a container and run it.
```
``` Build and first runWithout making any changes to dockerfile we go back to the terminal and run the suggested build command:
docker compose up –build
First build will take about a minute and consecutive will be a bit faster. Dcker performs the build and starts our container. You can see container’s status on Containers tab. You can see that our new container is up and running.
Which means we can can now open a web browser and type in the following URL: http://localhost:9000. This is what you are supposed to see:
Good news: our container is in fact running.
Bad news: mysqli extension is missing and we cannot connect to MySQL.
Installing mysqli extensionQuick Google search for “docker php install mysqli” points us to the solution and we modify our dockerfile in any text editor adding lines 7 and 8.
```
``` Now we stop the container, run the build command again and once finished we refresh our browser window. This takes us one step further but we are not quite there yet.
While this error is not very descriptive we can guess that our application is not able to connect to MySQL. We use “localhost” as MySQL server address which in case of the container refers to container itself. We need to find a way to connect to MySQL on the host which is our local computer running Docker desktop. Another quick Google search for “docker connect to host from container” points us to host.docker.internal which we need to use instead of localhost while connecting to MySQL.
We proceed to PHPRunner, create a new Server Database Connection on the ‘Output Directory’ screen and change the address of MySQL server as follows:
$host = 'host.docker.internal';$user = 'root';$pwd = '';$port = 3306;$sys\_dbname = 'cars';
And one more try1. Stop the container
2. Build PHPRunner project ( make sure that new connection is selected )
3. Copy all files from PHPRunner’s output folder to C:\Projects\Docker\src
4. Execute docker compose up –build command one more time.
5. Refresh browser’s window
It runs!
Additional notes. You can make your container connect to any database. It can be a database that is a part of the same container ( though it is not recommended ), or a database running in another container or any external database server. All you need to do is to point the database connection to your database of choice.
As you might know, we teamed up with PHPDesktop developer to bring it to the latest version of Chromium and PHP. Now we have PHPDesktop 130.1 at our disposal that runs PHP 8.3. It will be a part of PHPRunner 11 soon and meanwhile I will show you how to upgrade PHPDesktop in your PHPRunner 10.91 installation.
Download updated PHPDesktop. Unzip it to C:\Program Files\PHPRunner 10.91\DesktopApp keeping directory structure and overwriting existing files.
Download updated phprunnerapp-setup.iss file and replace the existing one in C:\Program Files\PHPRunner 10.91\DesktopApp folder.
This is it! Now you can use PHPRunner 10.91 to build desktop apps that support PHP 8.3.
Our goal is to display website visitors on the map, similar to the screenshot below.
We will convert their IP address to lat/lng coordinates and display those markers on OpenStreetMap map. To perform the conversion of IP addresses to lat/lng pairs we are going to use the geolocation data from ip2location.com.
We will display users that were active in the last ten minutes. If the user had some activity in the last 60 seconds, their dot will be pulsing.
There is also a YouTube video that provides more details of this project.
We are going to need two tables, ‘users’ and ‘ip2location’. The following is the script for MySQL database.
CREATE TABLE `ip2location`(`id` int NOT NULL AUTO\_INCREMENT, `ip\_start` decimal(20,6) NULL, `ip\_end` decimal(20,6) NULL, `STATE` varchar(50) NULL, `COUNTRY` varchar(50) NULL, `REGION` varchar(50) NULL, `CITY` varchar(100) NULL, `LATITUDE` double NULL, `LONGITUDE` double NULL, PRIMARY KEY (`id`))CHARACTER SET utf8;CREATE TABLE `users`(`id` int NOT NULL AUTO\_INCREMENT, `ip` varchar(50) NOT NULL DEFAULT '0', `lat` double NOT NULL DEFAULT 0, `lng` double NOT NULL DEFAULT 0, `last\_activity` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`))CHARACTER SET utf8;
Please note that this SQL script only creates ‘ip2location’ but doesn’t come with the data. The data set itself is about 300Mb and you can download it for free at https://lite.ip2location.com/database/ip-country.
PHP code:
echo "<div id='map' style=''></div>";
3. AfterApplicationInitialized event
PHP code:
// convert IP address to a decimal number in order to perform a database searchfunction ip\_to\_decimal($ip\_address) { $parts = explode('.', $ip\_address); $decimal\_ip = 0; foreach ($parts as $part) { $decimal\_ip = $decimal\_ip * 256 + (int) $part; } return $decimal\_ip;}function saveCurrentUserData(){$ip = $\_SERVER["REMOTE\_ADDR"];if( empty($ip) )return false;$userRs = DB::Select("users",array("ip" => $ip));$user = $userRs->fetchAssoc();if( $user ){DB::Update("users", array("last\_activity" => date("Y-m-d H:i:s")) ,array("ip" => $ip));}else{$decimalip = ip\_to\_decimal($ip);$coordsRs = DB::Query("select * from ip2location where ".$decimalip." BETWEEN ip\_start and ip\_end");$coords = $coordsRs->fetchAssoc();if( $coords ){$userData = array("ip" => $ip, "lat" => $coords["LATITUDE"], "lng" => $coords["LONGITUDE"], "last\_activity" => date("Y-m-d H:i:s"));DB::Insert("users",$userData);}}}if( postvalue("getActiveUsers") ){$interval = 10; // we only display on the map users that accessed any page in the last ten minutessaveCurrentUserData();$dateCondition = date("Y-m-d H:i:s",time() - ($interval*60));$userRs = DB::Query("select * from users where last\_activity > '".$dateCondition."'");$latLng = array(); $userData = $userRs->fetchAssoc();while( $userData ){$coordsInfo = array("id" => $userData['id'], "lat" => $userData['lat'], "lng" => $userData['lng'], "active" => false);if( ( time() - strtotime($userData["last\_activity"]) ) <=60 ) { $coordsInfo['active'] = true; }$latLng[] = $coordsInfo; $userData = $userRs->fetchAssoc();}echo my\_json\_encode($latLng);exit();}
4. custom_function.js
The following Javascript code goes to Event Editor -> custom_function.js section.
$(document).ready(function() { $("#map").width($(".r-fluid").width()); var height = $(window).height() - $("#map").offset().top - 30; $("#map").height(height); window.mapObj = new OpenLayers.Map("map", { controls: [ new OpenLayers.Control.PanZoomBar(), new OpenLayers.Control.Navigation() ], }); var layer = new OpenLayers.Layer.OSM(); mapObj.addLayer(layer); window.markersList = new OpenLayers.Layer.Markers("Markers"); mapObj.addLayer(markersList); mapObj.zoomToMaxExtent(); updateMarkers(); setInterval(updateMarkers, 5000); function updateMarkers() { $.post("", { getActiveUsers: true }, function(response) { var coordsArr = JSON.parse(response), activeIds = coordsArr.map(function(coords) { return coords.id; }), allIds = markersList.markers.map(function(marker) { return marker.id; }); $.each(coordsArr, function(i, latLon) { if (!allIds.includes(latLon.id)) { addMarker(latLon.id, latLon.lat, latLon.lng, latLon.active); } else { var curMarker = markersList.markers.find(function(marker) { return marker.id == latLon.id }); if (curMarker.active != latLon.active) { $(curMarker.icon.imageDiv).toggleClass("active", latLon.active); } } }); allIds = markersList.markers.map(function(marker) { return marker.id; }); for (var i = 0; i < allIds.length; i++) { if (allIds[i] != undefined && !activeIds.includes(allIds[i])) { var markerToRemove = markersList.markers.find(function(marker) { return marker.id == allIds[i] }); markersList.removeMarker(markerToRemove); } } function addMarker(id, lat, lng, active) { var lonLat = new OpenLayers.LonLat(lng, lat) .transform( new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984 mapObj.getProjectionObject() // to Spherical Mercator Projection ); var icon = new OpenLayers.Icon("", new OpenLayers.Size(15, 15)); var marker = new OpenLayers.Marker(lonLat, icon); if (active) { $(marker.icon.imageDiv).addClass("active"); } marker.id = id; marker.active = active; markersList.addMarker(marker); return marker; } }); } updateMarkers();});
5. CSS code ( Style Editor -> Modify CSS )
We use this CSS code to customize and prettify the default look of OSM map.
.olTileImage { filter: brightness(48%) contrast(256%);}[id^="OL\_Icon"] .olAlphaImg { background: white; cursor: pointer; border-radius: 100%; } [id^="OL\_Icon"].active .olAlphaImg { animation: pulse 2s infinite; box-shadow: 10px 10px 10px rgba(255,255,255, 0.7); width:20px !important; height: 20px !important; } @-webkit-keyframes pulse { 0% { -webkit-box-shadow: 10px 10px 10px rgba(255,255,255, 0.7); } 70% { -webkit-box-shadow: 0 0 0 10px rgba(255,255,255, 0); } 100% { -webkit-box-shadow: 0 0 0 0 rgba(255,255,255, 0); } } @keyframes pulse { 0% { -moz-box-shadow: 0 0 0 0 rgba(255,255,255, 0.7); box-shadow: 0 0 0 0 rgba(255,255,255, 0.7); } 70% { -moz-box-shadow: 0 0 0 10px rgba(255,255,255, 0); box-shadow: 0 0 0 10px rgba(255,255,255, 0); } 100% { -moz-box-shadow: 0 0 0 0 rgba(255,255,255, 0); box-shadow: 0 0 0 0 rgba(255,255,255, 0); } }
In this article we will show how you can easily create a clickable code snippet in a dashboard. Our snippet will display a number of customers in Customers table and clicking anywhere in snippet area will take us to the Customers table in question.
PHP
$header = "Number of customers";$number = DB::DBLookup("select count(*) from customers");echo "Customers in the database: ".$number;
C#
dynamic number = null;header = new XVar("Number of customers");number = XVar.Clone(DB.DBLookup(new XVar("select count(*) from customers")));MVCFunctions.Echo(MVCFunctions.Concat("Customers in the database: ", number));
2. Build and run your application. Proceed to the dashboard page, right click on that snippet and choose ‘Inspect’. What we are looking for an ID of the DIV that encloses our code snippet. In our case it will be dashelement_cloudflare_snippet1.
$( "#dashelement\_cloudflare\_snippet1" ).bind( "click", function() { location.href = Runner.pages.getUrl("customers","list");});
This is it. Enjoy!
In this article we will show how you can implement drag-n-drop between two dashboard elements. It makes more sense to drag and drop records between two grids. In this example we will be using two tables named orders and orders_archive.
This is how it is going to look in the generated application:
CREATE TABLE `orders`(`OrderID` int NOT NULL AUTO\_INCREMENT, `CustomerID` varchar(5) NULL DEFAULT NULL, `EmployeeID` int NULL DEFAULT NULL, `OrderDate` datetime NULL DEFAULT NULL, `RequiredDate` datetime NULL DEFAULT NULL, `ShippedDate` datetime NULL DEFAULT NULL, `ShipVia` int NULL DEFAULT NULL, `Freight` decimal(12,2) NULL DEFAULT 0.00, `ShipName` varchar(40) NULL DEFAULT NULL, `ShipAddress` varchar(60) NULL DEFAULT NULL, `ShipCity` varchar(15) NULL DEFAULT NULL, `ShipRegion` varchar(15) NULL DEFAULT NULL, `ShipPostalCode` varchar(10) NULL DEFAULT NULL, `ShipCountry` varchar(15) NULL DEFAULT NULL, `Complited` tinyint NOT NULL DEFAULT 0, `order` int NULL DEFAULT NULL, PRIMARY KEY (`OrderID`))CHARACTER SET utf8;CREATE TABLE `orders\_archive`(`OrderID` int NOT NULL AUTO\_INCREMENT, `CustomerID` varchar(5) NULL DEFAULT NULL, `EmployeeID` int NULL DEFAULT NULL, `OrderDate` datetime NULL DEFAULT NULL, `RequiredDate` datetime NULL DEFAULT NULL, `ShippedDate` datetime NULL DEFAULT NULL, `ShipVia` int NULL DEFAULT NULL, `Freight` decimal(12,2) NULL DEFAULT 0.00, `ShipName` varchar(40) NULL DEFAULT NULL, `ShipAddress` varchar(60) NULL DEFAULT NULL, `ShipCity` varchar(15) NULL DEFAULT NULL, `ShipRegion` varchar(15) NULL DEFAULT NULL, `ShipPostalCode` varchar(10) NULL DEFAULT NULL, `ShipCountry` varchar(15) NULL DEFAULT NULL, `Complited` tinyint NOT NULL DEFAULT 0, `order` int NULL DEFAULT NULL, PRIMARY KEY (`OrderID`))CHARACTER SET utf8;
2. Create a dashboard in PHPRunner or ASPRunner.NET and add those two tables there.
Things to change:
– var connectTable should contain the name of another table. In orders table code it should say “orders_archive” and vice versa.
– “dashboard_dashboard.php” URL of the dashboard page. In ASPRunner.NET it will be something like dashboard
– “OrderID” – name of the key column of the current table.
$("[data-grid-message]").hide();$("[data-location='grid']").show();var connectTable = "orders\_archive";if(pageObj.dashboard){ // to initialize a plugin we will use a container where there grid is placed on the dashboard var panel = $( " > tbody",pageObj.gridElem ).parents(".panel-body"); panel.sortable({ connectWith: "#dashelement\_"+connectTable+"\_grid"+pageObj.dashboard.id+" .panel-body", items: 'tbody .r-gridrow', helper: 'clone', opacity: 0.6, stop:function(e,ui){ if ($(e.target).has(ui.item).length) $(e.target).sortable("cancel"); }, receive: function( e, ui ) { // we moved a row from another grid var OrderID = ui.item.find("[data-fieldname='OrderID']").find("span").html(),order = []; // make a post with from and to table names and also with the OrderID that needs to be updated $.post("dashboard\_dashboard.php",{a:"replaceRow",from:connectTable, to:pageObj.tName,OrderID:OrderID},function(response){console.log(response); }); // Update sort order as well. Not necessary but it looks better this way $.each($(" > tbody",pageObj.gridElem).find(".r-gridrow"),function(i,row){ var orderId = $(row).find("[data-fieldname='OrderID']").find("span").html(); order.push({OrderID:orderId,order:(i+1)}); }); $.post("dashboard\_dashboard.php",{order:order,table:pageObj.tName}); /* update 'Displaying n - n of n' for both tables */ /* decrease count */ updateDetails( $("#dashelement\_"+connectTable+"\_grid"+ pageObj.dashboard.id+" [data-itemid='details\_found']") ,-1); /* increase count */ updateDetails( $("#dashelement\_"+pageObj.tName+"\_grid"+ pageObj.dashboard.id+" [data-itemid='details\_found']") ,1); } }); function updateDetails(details,number){ var html\_details = details.html(), result\_details = /Displaying (\d+) - (\d+) of (\d+)/g.exec(html\_details), dispay = result\_details[2],of = result\_details[3]; html\_details = html\_details.replace(/- (\d+)/g,"- "+(parseInt(dispay) + number)); html\_details = html\_details.replace(/of (\d+)/g,"of "+(parseInt(of) + number)); details.html(html\_details); }};
4. Dashboard page: BeforeProcess event
In this event we add a new record to the target table and delete the same in the source table. The second section of this code helps us to update the sort order so
PHP code
if( postvalue("a") === "replaceRow"){$fromRs = DB::Select(postvalue("from"),array("OrderID" => postvalue("OrderID")));$selectError = DB::lastError();$fromRow = $fromRs->fetchAssoc();if($fromRow){DB::Insert(postvalue("to"),$fromRow);DB::Delete(postvalue("from"),array("OrderID" => postvalue("OrderID")));}exit();}// update sort order if requiredif( postvalue("order") ){$order = postvalue("order") ;foreach($order as $orderInfo){DB::Update(postvalue("table"),array("order" => $orderInfo["order"]), array("OrderID" => $orderInfo["OrderID"]));}exit();}
C# code
if(XVar.Equals(XVar.Pack(MVCFunctions.postvalue(new XVar("a"))), XVar.Pack("replaceRow"))){dynamic fromRow = null, fromRs = null, selectError = null;fromRs = XVar.Clone(DB.Select((XVar)(MVCFunctions.postvalue(new XVar("from"))), (XVar)(new XVar("OrderID", MVCFunctions.postvalue(new XVar("OrderID"))))));selectError = XVar.Clone(DB.lastError());fromRow = XVar.Clone(fromRs.fetchAssoc());if(XVar.Pack(fromRow)){DB.Insert((XVar)(MVCFunctions.postvalue(new XVar("to"))), (XVar)(fromRow));DB.Delete((XVar)(MVCFunctions.postvalue(new XVar("from"))), (XVar)(new XVar("OrderID", MVCFunctions.postvalue(new XVar("OrderID")))));}MVCFunctions.ob\_flush();HttpContext.Current.Response.End();throw new RunnerInlineOutputException();}if(XVar.Pack(MVCFunctions.postvalue(new XVar("order")))){dynamic order = XVar.Array();order = XVar.Clone(MVCFunctions.postvalue(new XVar("order")));foreach (KeyValuePair<XVar, dynamic> orderInfo in order.GetEnumerator()){DB.Update((XVar)(MVCFunctions.postvalue(new XVar("table"))), (XVar)(new XVar("order", orderInfo.Value["order"])), (XVar)(new XVar("OrderID", orderInfo.Value["OrderID"])));}MVCFunctions.ob\_flush();HttpContext.Current.Response.End();throw new RunnerInlineOutputException();}return null;
Enjoy!
Infinite scroll is a useful technique that allows to load new content automatically, when the user scrolls down and reaches the end of the page. This is the kind of feature that our customers asked us about for a fairly long time. Turns out, it is not that difficult to implement with the help of some custom coding.
window.pagecount = 1;// adding load indicator under the grid$("[data-location='grid']").after("<span class='end\_message' style='visibility:hidden;font-weight:bold;margin-bottom:20px;display: block;text-align: center;'>End</span>");$("[data-location='grid']").after("<div class='load\_img' style='visibility:hidden;text-align:center;margin-bottom:10px;'><span style='font-weight:bold;margin-bottom:5px;clear:both;display:block;'>Loading</span><img src='images/indicator.gif' style='width:30px;height:30px;'></div>");// scroll event handler$(window).on("scroll", function() {// end of the document and we have more dataif ($(this).scrollTop() + 5 > $(document).height() - $(this).height()) {window.pagecount++;// show load indicator$(".load\_img").css("visibility", "visible");// get the next page contentRunner.runnerAJAX(Runner.pages.getUrl(pageObj.tName, pageObj.pageType) + "?goto=" + window.pagecount,pageObj.ajaxBaseParams,function(respObj) {// hide load indicator$(".load\_img").css("visibility", "hidden");// in respObj.html contains the whole new page HTML. We need to extract just the datavar grid\_tbody = $(respObj.html).find("[data-location='grid']").find("tbody");if (grid\_tbody.find("tr").length > 0) {// add new records to the grid$("[data-location='grid']").find("tbody").append(grid\_tbody.html());} else {// no new data, show 'End' message$(window).off("scroll");$(".end\_message").css("visibility", "visible");$(".load\_img").remove();}});}});
This is it. Enjoy!
In a web application, some long running tasks may time out and not finish. To avoid this kind of limitation we will show you how to run such tasks reliably and, at the same time provide progress status to the end user so they know the task is still running. In this article we’ll show you how to achieve this in PHPRunner and ASPRunner.NET.
A common task like this is batch sending email notifications. The key is to initiate a process like this from JavaScript sending the next batch of tasks to the server via AJAX.
1. Button on the List page.
First, we will add a new button to the Customers list page titled ‘Send emails’. The following code goes to ClientBefore event. Leave Server and ClientAfter events empty.
// how many emails to send per stepvar recCount = pageObj.proxy["recCount"], mail\_per\_step = 10;// basic HTML code for the progress popup windowvar html = "<div class><b>Sent mails: <span class='sent'>0</span> of <span>" + recCount + "</span></b></div>" + "<br><div>Mailed: <span class='mailed'>0</span>, Errors: <span class='errors'>0</span></div>";// showing the progress in a popupvar popup = Runner.displayPopup({html: html,header: 'Sending...',afterCreate: function (win) {// starting sending emails, ajaxStep is a secursive functionajaxStep(0, mail\_per\_step);}});function ajaxStep(step, mail\_per\_step) {$.get("", { ajaxMail: true, ajaxstep: step, mail\_per\_step: mail\_per\_step }, function (response) {var json = JSON.parse(response);// using the response data to update the progress popup$(".sent").html(parseInt($(".sent").html()) + json["totalSent"]);$(".mailed").html(parseInt($(".mailed").html()) + json["mailed"]);$(".errors").html(parseInt($(".errors").html()) + json["errors"])if (parseInt(json["totalSent"]) < mail\_per\_step) {// time to exit the recursion. The number of processed records is less than// number of records per step which means we ran out of data to processswal("Done");} else {// otherwise proceed to the next stepstep++;ajaxStep(step, mail\_per\_step);};});}return false;
2. List page, BeforeDisplay event.
In this event we simply calculate the number of records in the customers table and pass it to Javascript. This way our Javascript code knows how many emails to send.
Modify the SQL query to match your database structure.
PHP:
$sql = "select count(*) from customers";$recCount = DB::DBLookup($sql);$pageObject->setProxyValue("recCount",$recCount);
C#
dynamic recCount = null, sql = null;sql = new XVar("select count(*) from customers");recCount = XVar.Clone(DB.DBLookup((XVar)(sql)));pageObject.setProxyValue(new XVar("recCount"), (XVar)(recCount));
3. After Table Initialized event.
This is where we actually run our job.
PHP
if( postvalue("ajaxMail") != false ){$response = array("totalSent" => 0, "errors" => 0,"mailed" => 0);// number of records to process per step$recCount = postvalue("mail\_per\_step");// starting record$startRecord = intval(postvalue("ajaxstep"))*$recCount;// name of the email field$email\_field = "email";// sql query$sql = "select * from customers limit ".$startRecord.",".$recCount;$rs = DB::Query($sql);// the main cycle where we perform our taskswhile($data = $rs->fetchAssoc()){$mail\_params = array('to' => $data[$email\_field], 'subject' => "Ajax notification", 'htmlbody' => "body text");$mailed = runner\_mail($mail\_params);if($mailed["mailed"])$response["mailed"]++;else$response["errors"]++;$response["totalSent"]++;}// return the responseecho my\_json\_encode($response);exit();}
C#
if (MVCFunctions.postvalue(new XVar("ajaxMail")) != false) {dynamic data, rs, email\_field = null, mail\_params = null, mailed = XVar.Array(), recCount = null, sql = null, startRecord = null, var\_response = XVar.Array();var\_response = XVar.Clone(new XVar("totalSent", 0, "errors", 0, "mailed", 0));recCount = XVar.Clone(MVCFunctions.postvalue(new XVar("mail\_per\_step")));startRecord = XVar.Clone((int) MVCFunctions.postvalue(new XVar("ajaxstep")) * recCount);email\_field = new XVar("email");sql = XVar.Clone(MVCFunctions.Concat("select * from customers limit ", startRecord, ",", recCount));rs = XVar.Clone(DB.Query((XVar)(sql)));while (XVar.Pack(data = XVar.Clone(rs.fetchAssoc()))) {mail\_params = XVar.Clone(new XVar("to", data[email\_field], "subject", "Ajax notification", "htmlbody", "body text"));mailed = XVar.Clone(MVCFunctions.runner\_mail((XVar)(mail\_params)));if (XVar.Pack(mailed["mailed"])) {var\_response["mailed"]++;} else {var\_response["errors"]++;}var\_response["totalSent"]++;}MVCFunctions.Echo(MVCFunctions.my\_json\_encode((XVar)(var\_response)));MVCFunctions.ob\_flush();HttpContext.Current.Response.End();throw new RunnerInlineOutputException();}
Happy coding!
Dashboards excel at showcasing a multitude of valuable information on a single page. However, there are instances when a basic dashboard layout falls short in accommodating the desired volume of data on a single screen. In this article, we will demonstrate a method for amplifying information presentation through the use of a tabbed component. It’s important to note that this is not a built-in and we will need to write a bit of code.
Here is what we after:
Instructions1. Create a dashboard
Dashboard layout needs to conform the following structure. The first row needs to have a single cell with the code snippet. All other objects that you want to appear in a tabbed element should occupy rows below the code snippet. It doesn’t matter how many elements are in each row, they will be moved inside the tabbed element anyway. In our example we are going to display three grids, a single record, a chart and a report inside the tabbed element.
Here is a sample layout:
The idea of this code is to display an empty tabbed panel. Nothing special here, just a basic Bootstrap-based HTML code. The only important thing here is the ID of this panel which is dashTabs. We are going to use this ID in both Javascript and CSS code.
PHP
$header = "Tabs";echo "<div class='panel with-tabs panel-default form-tabs' id='dashTabs'><div class='panel-heading'><ul class='nav nav-tabs' role='tablist'></ul></div><div class='panel-body'><div class='tab-content'></div></div></div>";
C#
header = new XVar("Tabs");MVCFunctions.Echo("<div class='panel with-tabs panel-default form-tabs' id='dashTabs'>\r\n<div class='panel-heading'>\r\n<ul class='nav nav-tabs' role='tablist'>\r\n</ul>\r\n</div>\r\n<div class='panel-body'>\r\n<div class='tab-content'>\r\n</div>\r\n</div>\r\n</div>");
3. Dashboard Javascript OnLoad event
This code loops through the array of dashboard elements and moves them one by one to the tabbed panel. See inline comments for more info.
// an array with all the dashboard element that we want to display in individual tabsvar tabs\_pages = [ { table: "employees", page: "grid" }, { table: "customers", page: "grid" }, { table: "customers", page: "record" }, { table: "Bar Chart", page: "chart" }, { table: "order details", page: "grid" }, { table: "categories Report", page: "report" }];$.each(tabs\_pages, function () { var goodTableName = Runner.goodFieldName(this.table); var tabId = goodTableName + "\_" + this.page; // adding a new tab $(".nav", "#dashTabs").append("<li role='presentation'><a aria-controls='settings' role='tab' data-toggle='tab' href='#" + tabId + "'>" + this.table + " " + this.page + "</a></li>"); $(".tab-content", "#dashTabs").append("<div role='tabpanel' class='tab-pane' id='" + tabId + "'></div>"); var pageDashElement = $("#dashelement\_"+goodTableName+"\_"+this.page+pageid);// adding a CSS class for the new tab element pageDashElement.addClass("tabelement");// adding this new element to the tab $("#" + tabId).append(pageDashElement);});// make sure that the first tab is selected$("li[role='presentation']", "#dashTabs").first().find("a").click();
4. Custom CSS
This code goes to Style Editor -> Modify CSS section. Just hiding some unnecessary decoration and making tabs look prettier.
[data-itemtype="dashboard-item"].tabelement .panel-heading{ background: none; border: none;}[data-itemtype="dashboard-item"].tabelement .panel-heading .rnr-dbebrick{ color:#337ab7;}[data-itemtype="dashboard-item"].tabelement .panel-heading .rnr-dbebrick .btn{ background: #337AB7; color: white;}
Happy coding!
As many of you know, we recently launched a side project named PicTur. This is a website, built with the help of PHPRunner, that allows users build beautiful travel stories. Instead of letting you pictures collect dust you can create a travel story and share with the world. Take a look open an account and maybe create a story or two.
This website was built with the help of PHPRunner and was heavily customized. We will post series of articles talking about the most interesting parts of this project. Let us start by showing how pages like login, register and remind password were prettified. First, we found a good login template at Carrd, customized it a bit and imported CSS into PHPRunner.
This is the end result:
Login page layoutHere is how your login page is supposed to look in the Page Designer if you want to re-create the same in your project.
To add an arrow to the “Sign in” button select this button in the Page Designer and on the right side panel choose fa-arrow-right icon from Font Awesome set.
Header code snippetInsert a code snippet above the login fields and use the following code there:
echo "<h2>Page Header</h2>";echo "<h3>Logon to your account</h3>";
Custom CSSThe following code goes to Style Editor -> Custom CSS.
.function-login { line-height: 1.0; min-height: 100vh; min-width: 320px; overflow-x: hidden; word-wrap: break-word;}.function-login .panel-heading { display: none;}.function-login .bs-pagepanel { background: none;}.function-login:before { background-attachment: scroll; content: ''; display: block; height: 100vh; left: 0; pointer-events: none; position: fixed; top: 0; transform: scale(1); width: 100vw; z-index: -1; background-size: cover; background-position: center; background-repeat: no-repeat; background-image: linear-gradient(to top, rgba(235, 235, 235, 0.1), rgba(235, 235, 235, 0.2)), url(images/bg\_login.jpg);}.function-login .r-panel-form { --alignment: left; --flex-alignment: flex-start; --indent-left: 1; --indent-right: 0; /*display: flex;*/ width: 100%; align-items: center; justify-content: center; background-color: rgba(20, 20, 23, 0.788); -webkit-backdrop-filter: blur(0.6rem); backdrop-filter: blur(0.6rem); box-shadow: 0rem 0.875rem 2.125rem 0rem rgb(0 0 0 / 33%); border-radius: 1.25rem;}.function-login input[type="text"],.function-login input[type="password"] { height: 4.875rem; line-height: 4.875rem; padding: 0 1.18125rem; color: rgba(255, 255, 255, 0.761); background-color: rgba(20, 20, 23, 0.361); font-size: 1em; font-family: 'Inter', sans-serif; font-weight: 400; border-radius: 0.5rem; border: none; -webkit-box-shadow: none; box-shadow: none;}.function-login input[type="text"]:focus,.function-login input[type="text"]:active,.function-login input[type="password"]:focus,.function-login input[type="password"]:active { box-shadow: 0 0 0 1px #8571d1, inset 0 0 0 1px #8571d1;}.function-login .checkbox label { color: rgba(255, 255, 255, 0.761); padding-left: 0px;}.function-login .checkbox label:before { border-radius: 0.5rem; color: rgba(255, 255, 255, 0.761); background-color: rgba(20, 20, 23, 0.361); background-size: 1.434375rem; height: 2.53125rem; width: 2.53125rem; margin-right: 1.265625rem; background-position: center; background-repeat: no-repeat; content: ''; cursor: pointer; display: inline-block; flex-grow: 0; flex-shrink: 0; vertical-align: middle; box-shadow: 0 0 0 1px #4c4a55, inset 0 0 0 1px #4c4a55;}.function-login label.checked:before { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='648' height='552' preserveAspectRatio='none' viewBox='0 0 648 552'%3E%3Cpath d='M225.3,517.7L2.1,293.1l68.1-67.7L226,382.3L578.1,35.6l67.4,68.4L225.3,517.7z' fill='rgba(255,255,255,0.761)' /%3E%3C/svg%3E");}.function-login label:hover:before { box-shadow: 0 0 0 1px #8571D1, inset 0 0 0 1px #8571D1;}.function-login input[type="checkbox"] { display: none;}.function-login [data-itemtype="login\_remind"] a,.function-login [data-itemtype="loginform\_register\_link"] a { color: #FFFFFF; font-family: 'Inter', sans-serif; text-shadow: 0rem 0.125rem 0.25rem rgb(0 0 0 / 7%);}.function-login [data-itemtype="login\_remind"] a:hover,.function-login [data-itemtype="loginform\_register\_link"] a:hover,.function-login [data-itemtype="login\_remind"] a.active,.function-login [data-itemtype="loginform\_register\_link"] a.active { color: white !important;}.function-login .r-panel-page { min-width: auto; width: 100%; max-width: 100%; margin-top: 0px;}.function-login form { width: 50rem; margin: 0 auto;}.function-login h2 { color: #FFFFFF; font-family: 'Inter', sans-serif; font-size: 3.25em; line-height: 1.25; font-weight: 600; text-shadow: 0rem 0.125rem 0.25rem rgb(0 0 0 / 7%);}.function-login h3 { color: rgba(255, 255, 255, 0.902); font-family: 'Inter', sans-serif; letter-spacing: 0.025rem; width: calc(100% + 0.025rem); font-size: 1.3em; line-height: 1.75; font-weight: 400; text-shadow: 0.088rem 0.088rem 0.125rem rgb(0 0 0 / 19%);}.function-login tr:first-child .clearfix[edit-form-cell] { padding: 25px; display: flex; flex-direction: column; align-items: center;}.function-login tr:nth-child(2) .clearfix[edit-form-cell] { text-align: center;}.function-login .clearfix[edit-form-cell] .r-edit-field { margin-bottom: 10px; width: 100% !important; padding: 0px !important;}.function-login [data-itemtype="login\_button"] { width: 100%; margin-bottom: 10px;}.function-login .btn.btn-primary { text-align: center; width: 100%; height: 4.875rem; line-height: 4.875rem; padding: 0 1.9375rem; font-size: 1em; font-family: 'Inter', sans-serif; font-weight: 600; border-radius: 0.5rem; flex-direction: row-reverse; justify-content: flex-end; background-color: #7662C4; color: #FFFFFF; background-image: linear-gradient(142deg, rgba(221, 136, 235, 0.451) 0%, rgba(118, 98, 196, 0.008) 58%); background-position: 0% 0%; background-repeat: repeat; background-size: cover; transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease; position: relative; display: flex; align-items: center;}.function-login .btn.btn-primary .fa { order: 0; margin-left: auto;}.function-login .btn.btn-primary:hover { background-color: #8770E0;}.function-login .panel { -webkit-box-shadow: none; box-shadow: none; border: none;}.function-login .panel-primary { border-color: none;}.function-login [data-itemid="login\_google"] { margin-top: 20px;}body.user\_forms.function-register .help-block { margin-top: 10px;}@media (min-width: 320px) and (max-width: 768px) { .function-login form { max-width: calc(100% - 2.5rem + 0.4725px); width: calc(100% - 2.5rem + 0.4725px); }}.function-login [data-itemtype="login\_message"] { background: none; border: none; box-shadow: none;}.function-login tr:nth-child(2) * { color: white;}
Enjoy!
Version 11 is a big redesign that will significantly improve the user experience. We are switching from storing project data in XML and SQLite databases to a real database like MySQL or SQL Server. Also, switching from a slow Internet Explorer engine that powers our UI to Chrome.
Benefits Much faster UI Much faster work with large ( 1000+ tables ) projects Multi-user work! Revisions and roll-backs* Later: Mac/Linux versions
This will be a game-changer! Plus it won’t break project compatibility and all v10.x project will open and build without any issues. We estimate that version 11 will take about six months to build and to be available in the first half of year 2023. This is a long time but it worth the wait.
Also, switching to HTML-based UI will make PHPRunner and ASPRunner.NET wizard software much better looking. It opens possibilities like light and dark themes in the software itself.
The beta versions of PHPRunner and ASPRunner.NET 10.9 are here!
Download links:
PHPRunner 10.9 beta
ASPRunner.NET 10.9 beta
Please note that this is a beta version and is not meant to be used in production.
This new version features the following improvements:
Excel-like filters in field headers on List pageThe feature many of you asked for and now it is here.
To enable it proceed to the Page Designer, open the List Page, click on the fields in the grid header and on the right-side settings panel select ‘Filter’ and ‘Apply to all fields’. There also will be additional ways to enable this feature as well.
Users can now enable multiple 2FA methodsEach user can now select which 2FA methods they can use to protect their account.
New Menu API functionalityMenu elements can be added or removed in the custom code. You can, for instance, load the whole menu from the database now.
Improved page load speedThis is being achieved by reducing number of JS and CSS files, something that we need to do from time to time to keep the page load speed under control.
Notifications API updateImplemented fine-tuned permissions system to define who can see notifications:
– a specific user only
– specific group members only
– only users logged via Active Directory or via Google
– only users who have access to a specific page
– only users who have access to a specific record
New totals optionsYou can choose between displaying current page totals and all data in the table totals.
Added Swal2 library for popupsNow you can simply use any code example from Swal2 library.
Upgraded jQuery libraryThe latest stable version of jQuery 3.6 is now bundled with PHPRunner and ASPRunner.NET.
Another minor upgrade is planned in January-February– SQL and REST Views functionality improvements
– a bunch of minor improvements and fixes related to security, login and registration pages
Enjoy!
Our website xlinesoft.com was down from May 24, 2019 to May 30, 2019.
First, I noticed that I cannot logon to our online helpdesk. Then the website itself started showing ads that we never had. We assumed that our server was hacked but it turned out it simply points to a different IP address now. Hacker downloaded a static copy of our website, added some ads and tried to make some money via AdSense.
I tried to logon to GoDaddy account and check DNS settings. The login didn’t work and the password reset email never arrived. Our account at GoDaddy was hacked and attackers сhanged domain name ownership data. I was relieved though, dealing with GoDaddy should be easier than negotiating with a hacker, right?
GoDaddy sagaOver these six days, I spent a dozen of hours on the phone with GoDaddy. Unfortunately, the only way to contact their fraud department is via the form on the website and they will take up to 72 hours to get back to you. So every time I submitted supporting documents I would call a regular support line and I ask them to contact someone from the fraud department and check the status of our case.
I have got exactly two one-liner replies from the fraud department over these six days and they were nothing but a joke.
This is what we got back on Day 2:
We see you recently submitted a Change Update request. We’re sorry, but this department can only make this change after verifying the consent of the registrant or account holder – and unfortunately, the consent was not provided in this case. You are not the account holder or registrant as currently recorded and no business documentation was submitted for consideration.
So they basically telling us that they contacted the hacker and the hacker didn’t agree to return our domains. What a surprise! It worth saying that all documents were provided like a scan of drivers license, company registration and Xlinesoft.com DBA (doing business as) registration.
After hours on the phone with customer support and resubmitting all the same documents we got a second reply on Day 5:
Thank you for your email. Unfortunately, we are unable to give out account information, without proper validation.
How did we get it backAt some point, we realized that GoDaddy won’t help us in any way. We were working with the lawyer to send a formal complaint to GoDaddy, to ICANN and, possibly, to law enforcement. On day five something unexpected – the hacker contacted us.
“I have your domains,” he said, “I can give it back to you for 1000 dollars”. I knew right away that he was the one, it came from the email address specified in WHOIS database as a new owner. After a few emails back and forth we decided to give it a try. And of course, he wanted money to be sent to his Bitcoin address. Luckily we had a friend who had some Bitcoins ready and this was the first time I used crypto for something useful.
The conversation with the hacker was somewhat amusing:
Please send money First otherwise i sold this domain to darkweb. there are many hackers on darkweb he can buy this domain in good rate. and Godaddy cant do anything. i am a ethical hacker and i am a muslim. i promise you when you send me a money within 1 minutes i trasfer your domain into your account . Trust Me ! .
We sent the first half and got the first domain back plus access to our account. After sending the rest we got the second domain back as well. The hacker knew what he was doing. Right after getting access to our account he transferred domains to another account at GoDaddy. Even if GoDaddy did their job and restored access to our account it would have been empty and another investigation should have been started to track those domains down.
Anyway, he did what he promised and transferred domains back to our account. Sadly enough, dealing with the hacker was more pleasant than dealing with GoDaddy. Maybe because he was an ethical hacker.
And just to give you an idea of how common this kind of crime is – we have checked all incoming transactions to his Bitcoin address. He earned about USD $50,000 since the beginning of the year. Not bad for someone living in the rural Pakistan.
Lessons learnedSo it was nothing but my own stupidity that led to this snafu. It is easy to forget basic security rules when you only use some website maybe once a year. Still, it is a terrible excuse. Don’t let this happen to you.
Just a quick reminder of what needs to be done.
– Use two-factor authentication everywhere where you have anything of importance.
– Do not reuse passwords. Our account was broken in because the same password was used on some other website that had their passwords stolen.
– Do not log in using an email address from the same domain. If something happens to your domain you won’t be able to access your email and won’t be able to restore your password.
– Do not use GoDaddy. If things go south you are basically on your own.
Special offer: Buy one get one free Time Remaining: To take advantage of this offer place an order for any eligible product and contact support team with your order number to claim your gift. Buy two products – get two free gifts etc. This offer applies to both new purchases and upgrades. Here is the list of eligible gifts: PHPRunner ASPRunner.NET ASPRunnerPro Templates pack Quiz Template Survey template Document Management template Invoice template EmailReader template MassMailer template Forum template WordPress template Meetings template...Continue Reading "Black Friday – Cyber Monday sale" →
In this article we will show you how to generate PDF invoices on the web server side. We use this approach to generate and email billing reminders to our customers. 1. Download NodeJS to the web server. For Windows choose 64-bit MSI installer. 2. Run and install keeping all default settings. 3. Inside your project create a new folder named pdfmake. 4. Proceed to that folder, start the command line and run: npm install pdfmake 5. This will create index.js, in this file we will...Continue Reading "Generate PDF files using NodeJS and PDFMake" →
By Jerry Adach, Director Enterprise Data and Automation, Central Maine Healthcare ASPRunner.Net is, in my opinion, the best software on the market to build secure, role-based data driven workflows and complex reports and dashboards. Over the years, we started to use more and more stored procedures to be called by custom buttons and table events. It seemed to be more efficient having SQL Server handle some of the heavy lifting and more complicated tasks. Now we are using another platform to perform some heavy lifting...Continue Reading "Low-code and RPA automation" →
An old programmer's saying, coined by Jamie Zawinski, says “Every program attempts to expand until it can read mail". Jokes aside, email was and still is an integral part of our lives, and every web application needs to send emails to its users. This guide will walk you through all the steps to ensure every single email-sending aspect is covered. This is going to be a long article and we plan to update it often. Here are the topics, that will be covered in this...Continue Reading "A Complete Guide to Sending Emails with a Web-based Application" →
People often ask us this: "I found a great-looking theme on the Internet, how do I import it into PHPRunner or ASPRunner.NET". The problem is that all themes are implemented differently, there is no standard they follow and they cannot be "imported". However, it is possible to make your project look exactly like any of those themes and this article will teach you how to do this. For the inspiration, we will be using Material Dashboard Dark Edition theme by Creative Tim. Here is how...Continue Reading "How to create a beautiful dashboard theme" →
When you have a long List page with dozens of records, editing or viewing a record on a separate can be cumbersome. After you edit or view a record, clicking 'Back to List' will display all the records from the top and you lost the position of the record you just edited. This simple technique will allow you to scroll the List page back to the original position and also it will highlight the record that was just edited or viewed. To implement this feature...Continue Reading "How to scroll List page to the record that was edited" →
PHPRunner and ASPRunner.NET 10.8 are here! Trial version download links If you purchased PHPRunner or ASPRunner.NET less than 12 months ago, proceed to the control panel and download the registered version 10.8 there under 'My purchases'. Use 'Reg info' link next to your latest purchase. This new version features the following improvements: 1. New dashboards look and customization options There are many ways to configure and style dashboards in version 10.8. This is just one of them. You can also fully customize the appearance of...Continue Reading "Version 10.8" →
So you want to know how much time users spend on any specific page of your web application? This article explains how to log what pages your users visit and how much time they spend on each page. This kind of data can provide valuable insight into what forms of your application are too complicated and need to be split into several smaller forms. Or if they keep coming back to the welcome page this may mean your navigation inside the app can be improved....Continue Reading "Tracking visitors behaviour in your web application" →
Task - implement custom grid display in PHPRunner or ASPRunner.NET applications on mobile screens. The idea is to use the same HTML and achieve our goal using CSS only. CSS Grid layout is nothing new and excellent tutorials are available on the web for those who want to learn more. In this article we will only cover all the relevant details to PHPRunner and ASPRunner.NET. In this article, we will be using our Forum template as an example of using CSS Grid Layout. Desktop version...Continue Reading "Using CSS grid for mobile screens – a complete tutorial" →
This is an example of work we did for one of our clients. This kind of approach will work with any AnyChart chart that PHPRunner and ASPRunner.NET do not support directly. Here is how you can approach this kind of task. A Scatter chart is a graph that represents the relationship between two variables in a data set. Normally data is stored in the database as a set of (x,y) pairs. Here is the end result our client was looking for. This is how it...Continue Reading "Building a connected scatter chart" →
The main difference no-code and low-code applications is that you can easily extend low-code applications by adding your own code. This gives you both power and responsibility and we are going to talk about some typical mistakes people do while adding their own code. Let me show you an example of the code one our clients were using in BeforeLogin event: $rs = DB::Query("select * from users where username like '".$username."'"); $data = $rs->fetchAssoc(); ... Can you tell what is wrong here? If not, keep...Continue Reading "Preventing SQL injection in low-code web applications" →
When you build a public-facing application you need to make sure it looks sharp. In this article, we'll show you a few ideas of how you can make PHPRunner and ASPRunner.NET projects look unique. List page grid Let's start with the grid on the List page. As an example, we will be using a recently updated News template that comes with PHPRunner and ASPRunner.NET. We want to draw attention to the very first element, making it bigger than others. Luckily, it is fairly easy to...Continue Reading "Building visually appealing web applications" →
Welcome to DevQuest! We have built a little quest that is both fun and educational and dedicated to the topic of web development. There are eight questions total. Most of them are quite simple but some will require a bit of thinking. All of these questions can be answered with the help of your web browser and developer tools. We recommend using Google Chrome and Chrome Developer Tools but other browsers offer similar functionality. You would need to view the page source, use Javascript console,...Continue Reading "DevQuest contest with prizes" →
Version 10.7 of PHPRunner and ASPRunner.NET is here! Trial version download links If you purchased PHPRunner or ASPRunner.NET less than 12 months ago, proceed to the control panel and download the registered version 10.7 there under 'My purchases'. Use 'Reg info' link next to your latest purchase. This new version features the following improvements: 1. Files upload to cloud providers: Google Drive, OneDrive, Amazon S3, Dropbox 2. Notification API Let's dig into new functionality. Files upload to cloud providers When you set 'Edit as' type...Continue Reading "Version 10.7" →