<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Xlinesoft Blog</title>
	<atom:link href="https://xlinesoft.com/blog/feed/" rel="self" type="application/rss+xml" />
	<link>https://xlinesoft.com/blog</link>
	<description>Building the best web application builder, one byte at the time</description>
	<lastBuildDate>Thu, 10 Sep 2026 20:53:48 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>
	<item>
		<title>Experimenting with MCP Support in PHPRunner</title>
		<link>https://xlinesoft.com/blog/2026/09/10/experimenting-with-mcp-support-in-phprunner/</link>
					<comments>https://xlinesoft.com/blog/2026/09/10/experimenting-with-mcp-support-in-phprunner/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Thu, 10 Sep 2026 20:53:17 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Tutorials]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3498</guid>

					<description><![CDATA[The Model Context Protocol, or MCP, provides a standard way for AI assistants to connect to applications, discover available tools, and work with live data. We created an early MCP prototype to explore how this technology could work inside a generated PHPRunner application. Watch the PHPRunner MCP demonstration on YouTube Download the PHPRunner MCP sample project MCP tools in PHPRunner The prototype can expose standard operations for enabled application tables, including: &#8211; Listing and searching records &#8211; Retrieving individual records &#8211; Adding and updating records...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2026/09/10/experimenting-with-mcp-support-in-phprunner/" class="more-link">Continue Reading <span class="screen-reader-text">"Experimenting with MCP Support in PHPRunner"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>The Model Context Protocol, or MCP, provides a standard way for AI assistants to connect to applications, discover available tools, and work with live data.</p>
<p>We created an early MCP prototype to explore how this technology could work inside a generated PHPRunner application.</p>
<p><a href="https://youtu.be/EJun7UB42PM"><i class="fas fa-link" style="solid"></i> Watch the PHPRunner MCP demonstration on YouTube</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/09/MCP_Server_20260910173848622.zip"><i class="fas fa-link" style="solid"></i> Download the PHPRunner MCP sample project</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28.png"><img fetchpriority="high" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28-1024x576.png" alt="" width="810" height="456" class="alignnone size-large wp-image-3500" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28-1024x576.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28-600x338.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28-768x432.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28-1536x864.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/exec-7b54001f-8aa2-45f5-9883-81b9f5a19c28.png 1672w" sizes="(max-width: 810px) 100vw, 810px" /></a><br />
<span id="more-3498"></span></p>
<p><strong>MCP tools in PHPRunner</strong></p>
<p>The prototype can expose standard operations for enabled application tables, including:<br />
&#8211; Listing and searching records<br />
&#8211; Retrieving individual records<br />
&#8211; Adding and updating records<br />
&#8211; Deleting records</p>
<p>The available tools depend on the application configuration and permissions associated with the supplied API key.</p>
<p>An AI assistant can process requests such as “Show me the last five orders” or “Find all products in the Pastry category,” select the appropriate tool, and return the results in a readable format.</p>
<p><strong>Authentication and permissions</strong></p>
<p>This initial prototype uses API-key authentication. OAuth and a separate authorization server are not required.</p>
<p>The MCP layer works with the PHPRunner application layer responsible for permissions, validation, events, and data access. This is especially important for operations that change data. An API key can allow an assistant to view records without granting permission to add, update, or delete them.</p>
<p><strong>Custom MCP actions</strong></p>
<p>Applications often include workflows that require more than a single database operation. Custom MCP actions can represent these higher-level business processes.<br />
Multiple actions can be defined in an mcp_actions.php file. Each action describes its parameters, required permissions, behavior, and PHP handler.</p>
<p>The sample application includes actions for:<br />
&#8211; Producing an inventory summary<br />
&#8211; Receiving inventory<br />
&#8211; Discontinuing or reactivating products<br />
&#8211; Marking orders as shipped<br />
&#8211; Creating a customer and complete order<br />
&#8211; Increasing prices within a category</p>
<p>For example, creating a customer and order requires coordinated changes to several tables. A custom action can complete the entire workflow from one request.</p>
<p>MCP opens some interesting possibilities for conversational queries, application automation, reporting, and multi-step business workflows. Let us know which PHPRunner workflows you would like to access through an AI assistant.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/09/10/experimenting-with-mcp-support-in-phprunner/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Add a Custom Row Actions Menu to a Data Grid</title>
		<link>https://xlinesoft.com/blog/2026/09/05/add-a-custom-row-actions-menu-to-a-data-grid/</link>
					<comments>https://xlinesoft.com/blog/2026/09/05/add-a-custom-row-actions-menu-to-a-data-grid/#comments</comments>
		
		<dc:creator><![CDATA[Sergey Kornilov]]></dc:creator>
		<pubDate>Sat, 05 Sep 2026 16:42:34 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Tutorials]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3484</guid>

					<description><![CDATA[Create a compact per-record actions menu in PHPRunner and ASPRunner.NET with View, Edit, Copy, related-record navigation, permission-checked Delete, and room for custom actions.]]></description>
										<content:encoded><![CDATA[<p>List pages often contain several actions for every record. Displaying each action as a separate button can make the grid unnecessarily wide, especially on smaller screens.</p>
<p>In this tutorial, we will add one menu button to each grid row. The menu will open View, Edit, Copy, and related-record pages, process Delete with confirmation, and include an Archive placeholder for project-specific code. The same approach works in PHPRunner and ASPRunner.NET.</p>
<p><img decoding="async" class="alignnone size-full wp-image-3485" src="https://xlinesoft.com/blog/wp-content/uploads/2026/09/row-actions-menu-data-grid.png" alt="Custom row actions menu on a data grid" width="1160" height="625" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/09/row-actions-menu-data-grid.png 1160w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/row-actions-menu-data-grid-600x323.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/row-actions-menu-data-grid-1024x552.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/09/row-actions-menu-data-grid-768x414.png 768w" sizes="(max-width: 1160px) 100vw, 1160px" /></p>
<p><span id="more-3484"></span></p>
<h3>1. Adding the row actions button</h3>
<p>Open the required List page in <strong>Page Designer</strong> and switch the grid to Advanced Grid mode. Select <strong>Insert &#8211; Custom button &#8211; New button</strong>, then place the button inside the grid so every record receives its own copy.</p>
<p>Clear the button label, select the bars icon, and set its Item ID to <strong>row_actions</strong>. The Item ID is important because the JavaScript uses it to find the button belonging to the current record.</p>
<p>This example uses the <strong>customers</strong> table, <strong>id</strong> as its key field, and <strong>CustomerID</strong> to open related orders. Replace those names where necessary. The table must have a key field selected on the Choose pages screen.</p>
<p>Keep the standard Edit button in the grid with the Item ID <strong>grid_edit</strong>. The menu will trigger that button, preserving the Edit behavior configured in Page Designer, including popup mode.</p>
<h3>2. Building the menu in Client Before</h3>
<p>Open the custom button code and add the following JavaScript to <strong>Client Before</strong>:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
$(".phprunner-row-actions-menu").remove();

var btn = pageObj.getItemButton("row_actions", rowData.id);

var menu = $(
    '<div class="phprunner-row-actions-menu">' +
        '<div class="row-action" data-action="view">View</div>' +
        '<div class="row-action" data-action="edit">Edit</div>' +
        '<div class="row-action" data-action="orders">View orders</div>' +
        '<div class="row-action" data-action="delete">Delete</div>' +
        '<div class="row-action" data-action="copy">Create a copy</div>' +
        '<div class="row-action" data-action="archive">Archive</div>' +
    '</div>'
);

$("body").append(menu);

var pos = btn.offset();
var top = pos.top;
var left = pos.left + btn.outerWidth() + 4;

if (
    pos.top - $(window).scrollTop() + menu.outerHeight()
        > $(window).height()
) {
    top = pos.top - menu.outerHeight() + btn.outerHeight();
}

if (
    left + menu.outerWidth()
        > $(window).scrollLeft() + $(window).width()
) {
    left = pos.left - menu.outerWidth() - 4;
}

menu.css({
    top: top,
    left: left
});

menu.on("click", ".row-action", function(event) {
    event.stopPropagation();

    var action = $(this).data("action");
    menu.remove();

    switch (action) {
        case "view":
            Runner.displayPopup({
                url: Runner.pages.getUrl("customers", "view")
                    + "?editid1="
                    + encodeURIComponent(rowData.keys[0]),
                width: 800,
                height: 600,
                header: "View customer"
            });
            break;

        case "edit":
            pageObj.getItemButton(
                "grid_edit",
                rowData.id
            ).click();
            break;

        case "orders":
            var url = Runner.pages.getUrl("orders", "list");
            url += "?masterkey1="
                + encodeURIComponent(
                    row.getFieldValue("CustomerID")
                )
                + "&mastertable=customers";
            location.href = url;
            break;

        case "delete":
            swal("Would you like to delete this record?", {
                buttons: {
                    cancel: "No",
                    proceed: {
                        text: "Yes, delete it",
                        value: "proceed"
                    }
                }
            }).then(function(value) {
                if (value === "proceed") {
                    params["action"] = "delete";
                    submit();
                }
            });
            break;

        case "copy":
            location.href = Runner.pages.getUrl(
                "customers",
                "add"
            ) + "?copyid1="
                + encodeURIComponent(rowData.keys[0]);
            break;

        case "archive":
            // Add the code that archives the current record here.
            break;
    }
});

var closeTimer;

menu.on("mouseleave", function() {
    closeTimer = setTimeout(function() {
        menu.remove();
    }, 300);
});

menu.on("mouseenter", function() {
    clearTimeout(closeTimer);
});

return false;</textarea></pre>
</div>
<p>The button and the current record are identified through <strong>rowData.id</strong> and <strong>rowData.keys</strong>. If the menu would extend beyond the bottom or right side of the browser window, the positioning code moves it to the opposite side of the button.</p>
<p>The Copy action opens the Add page in Copy mode. This example has one key field, so it uses <strong>copyid1</strong>. Tables with composite keys also require <strong>copyid2</strong>, <strong>copyid3</strong>, and so on.</p>
<h3>3. Processing Delete on the server</h3>
<p>The Delete action is the only menu command that changes the database. Add the appropriate code to the custom button&#8217;s <strong>Server</strong> section.</p>
<p>The code verifies the current user&#8217;s Delete permission and obtains the record from the server-side button context. It does not trust a record ID supplied by the browser.</p>
<p><strong>PHP</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$result["action"] = "";

if ($params["action"] === "delete") {
    $rights = Security::getPermissions("customers");

    if (empty($rights["D"])) {
        $result["message"] =
            "You do not have permission to delete this record.";
    } else {
        $record = $button->getCurrentRecord();

        if (!$record) {
            $result["message"] = "The record could not be found.";
        } else {
            $sql = DB::PrepareSQL(
                "DELETE FROM customers WHERE id=:1",
                $record["id"]
            );

            if (DB::Exec($sql)) {
                $result["action"] = "delete";
            } else {
                $result["message"] =
                    "The record could not be deleted.";
            }
        }
    }
}</textarea></pre>
</div>
<p><strong>C#</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
result["action"] = "";

if (Convert.ToString(parameters["action"]) == "delete")
{
    dynamic rights = Security.getPermissions("customers");

    if (!XVar.Pack(rights["D"]))
    {
        result["message"] =
            "You do not have permission to delete this record.";
    }
    else
    {
        dynamic record = button.getCurrentRecord();

        if (!XVar.Pack(record))
        {
            result["message"] = "The record could not be found.";
        }
        else
        {
            dynamic sql = DB.PrepareSQL(
                "DELETE FROM customers WHERE id=:1",
                record["id"]
            );

            if (XVar.Pack(DB.Exec(sql)))
            {
                result["action"] = "delete";
            }
            else
            {
                result["message"] =
                    "The record could not be deleted.";
            }
        }
    }
}</textarea></pre>
</div>
<p>Replace <strong>customers</strong> and <strong>id</strong> with the actual table and key field. If the database prevents deleting records that have related rows, either configure the required cascade behavior or replace Delete with the business rule appropriate for your application.</p>
<h3>4. Refreshing the List page in Client After</h3>
<p>Add the following JavaScript to <strong>Client After</strong>:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
if (result["message"]) {
    swal(result["message"]);
}

if (result["action"] === "delete") {
    pageObj.reload({
        a: "reload"
    });
}</textarea></pre>
</div>
<p>The grid reloads only after the server confirms that the record was deleted.</p>
<h3>5. Styling the actions menu</h3>
<p>Open <strong>Style Editor</strong>, click &#8216;Modify CSS&#8217;, and add the following code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
.phprunner-row-actions-menu {
    position: absolute;
    z-index: 10000;
    min-width: 180px;
    padding: 6px 0;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 6px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
}

.phprunner-row-actions-menu .row-action {
    padding: 8px 14px;
    cursor: pointer;
    white-space: nowrap;
}

.phprunner-row-actions-menu .row-action:hover {
    background: #f5f5f5;
}</textarea></pre>
</div>
<p>After rebuilding the project, open the Customers List page and test the menu on several rows. Confirm that View opens in a popup, Edit follows the configured Edit-button behavior, Copy opens a prefilled Add page, related orders open for the correct customer, and Delete refreshes the grid only after confirmation.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/09/05/add-a-custom-row-actions-menu-to-a-data-grid/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Add Search Boxes to Individual Dashboard Grids</title>
		<link>https://xlinesoft.com/blog/2026/09/01/add-search-boxes-to-individual-dashboard-grids/</link>
					<comments>https://xlinesoft.com/blog/2026/09/01/add-search-boxes-to-individual-dashboard-grids/#respond</comments>
		
		<dc:creator><![CDATA[Sergey Kornilov]]></dc:creator>
		<pubDate>Tue, 01 Sep 2026 16:21:43 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Tutorials]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3481</guid>

					<description><![CDATA[Add independent search boxes to dashboard grid elements in PHPRunner and ASPRunner.NET using JavaScript, AJAX grid reloads, and responsive CSS.]]></description>
										<content:encoded><![CDATA[<p>PHPRunner and ASPRunner.NET dashboards can display several grids on one page. In this tutorial, we will add a separate search box to each grid so users can filter Customers, Products, or Employees without affecting the other dashboard elements.</p>
<p>The solution uses one reusable JavaScript function and a small amount of Custom CSS. The same code works in PHPRunner and ASPRunner.NET.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search.png"><img decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search-1024x453.png" alt="" width="810" height="358" class="alignnone size-large wp-image-3482" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search-1024x453.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search-600x266.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search-768x340.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search-1536x680.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/scr_dashboard_search.png 1843w" sizes="(max-width: 810px) 100vw, 810px" /></a></p>
<p><span id="more-3481"></span></p>
<h3>1. Preparing the dashboard</h3>
<p>Create a dashboard and add the grid elements that need their own search boxes. This example uses the <strong>customers</strong>, <strong>products</strong>, and <strong>employees</strong> data sources.</p>
<p>You will pass these data source names to the search function in section 3. Use the names as they appear in your project, not the visible dashboard panel captions.</p>
<h3>2. Adding the dashboard grid search function</h3>
<p>Open <strong>Events → custom_function.js</strong> and add the following JavaScript:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
function addDashboardGridSearch(dashboard, tableName) {
    var dashboardElement = dashboard.elements.find(function(element) {
        return element.table === tableName;
    });

    if (!dashboardElement) {
        console.warn("Dashboard table not found: " + tableName);
        return;
    }

    var containerId =
        "dashelement_" + dashboardElement.elementName + dashboard.id;
    var container = document.getElementById(containerId);

    if (!container) {
        console.warn("Dashboard container not found: " + containerId);
        return;
    }

    if (container.getAttribute("data-grid-search-initialized")) {
        return;
    }

    container.setAttribute("data-grid-search-initialized", "1");

    var searchValue = "";

    function search(input) {
        searchValue = input.value.trim();

        var parameters = {
            goto: 1
        };

        if (searchValue) {
            parameters.qs = searchValue;
        } else {
            parameters.a = "return";
            parameters.qs = "";
        }

        dashboard.loadPage(
            dashboardElement,
            {
                baseParams: parameters
            }
        );
    }

    function insertSearchBox() {
        var heading = container.querySelector(".panel-heading");
        var counter = container.querySelector(
            "[data-itemtype='details_found']"
        );

        if (!heading || !counter) {
            return;
        }

        if (heading.querySelector(".dashboard-grid-search")) {
            return;
        }

        var searchBox = document.createElement("div");
        var input = document.createElement("input");
        var clearButton = document.createElement("button");

        searchBox.className = "dashboard-grid-search";

        input.type = "search";
        input.className = "form-control input-sm";
        input.placeholder = "Search";
        input.value = searchValue;
        input.autocomplete = "off";
        input.setAttribute("aria-label", "Search " + tableName);

        clearButton.type = "button";
        clearButton.className = "btn btn-default btn-sm";
        clearButton.innerHTML = "&times;";
        clearButton.title = "Clear search";
        clearButton.setAttribute(
            "aria-label",
            "Clear " + tableName + " search"
        );

        searchBox.appendChild(input);
        searchBox.appendChild(clearButton);

        heading.classList.add("dashboard-grid-search-heading");
        counter.parentNode.classList.add("dashboard-grid-search-count");

        heading.insertBefore(searchBox, counter.parentNode);

        [
            "click",
            "mousedown",
            "mouseup",
            "touchstart"
        ].forEach(function(eventName) {
            searchBox.addEventListener(eventName, function(event) {
                event.stopPropagation();
            });
        });

        input.addEventListener("keydown", function(event) {
            if (event.key !== "Enter" && event.keyCode !== 13) {
                return;
            }

            event.preventDefault();
            event.stopPropagation();
            search(input);
        });

        clearButton.addEventListener("click", function(event) {
            event.preventDefault();
            event.stopPropagation();

            input.value = "";
            search(input);
        });
    }

    insertSearchBox();

    new MutationObserver(insertSearchBox).observe(
        container,
        {
            childList: true,
            subtree: true
        }
    );
}</textarea></pre>
</div>
<p>The function finds one dashboard element by its data source name and adds a search control to that element&#8217;s panel heading. Pressing Enter passes the search phrase in <strong>qs</strong> and reloads only that grid. The clear button removes the stored search.</p>
<p>Dashboard grids replace their markup after an AJAX refresh. The <strong>MutationObserver</strong> restores the search box after that replacement while preserving the current search phrase.</p>
<h3>3. Enabling search for the required grids</h3>
<p>Open <strong>Events → Dashboard → Dashboard page → JavaScript OnLoad</strong> and call the function once for each grid:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
addDashboardGridSearch(pageObj, "customers");
addDashboardGridSearch(pageObj, "products");
addDashboardGridSearch(pageObj, "employees");</textarea></pre>
</div>
<p>Replace these values with the data source names used by your dashboard. Omit any dashboard element that should not have an individual search box.</p>
<h3>4. Styling the search boxes</h3>
<p>Open <strong>Style Editor</strong>, click &#8216;Modify CSS&#8217;, and add the following code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
.function-dashboard .panel-heading.dashboard-grid-search-heading {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: nowrap;
}

.function-dashboard
.panel-heading.dashboard-grid-search-heading
> .rnr-dbebrick {
    float: none !important;
    white-space: nowrap;
}

.function-dashboard .dashboard-grid-search {
    display: flex;
    flex: 0 1 180px;
    min-width: 120px;
    margin-left: auto;
}

.function-dashboard .dashboard-grid-search .form-control {
    height: 30px;
    min-width: 0;
    padding: 4px 8px;
    font-size: 12px;
    border-radius: 4px 0 0 4px;
}

.function-dashboard .dashboard-grid-search .btn {
    height: 30px;
    padding: 4px 9px;
    font-size: 16px;
    line-height: 16px;
    border-radius: 0 4px 4px 0;
}

.function-dashboard .dashboard-grid-search-count {
    flex: 0 0 auto;
    margin-left: 0;
    white-space: nowrap;
    font-size: 12px;
}

@media (max-width: 900px) {
    .function-dashboard
    .panel-heading.dashboard-grid-search-heading {
        flex-wrap: wrap;
    }

    .function-dashboard .dashboard-grid-search {
        order: 3;
        flex-basis: 100%;
        margin-left: 28px;
    }

    .function-dashboard .dashboard-grid-search-count {
        margin-left: auto;
    }
}</textarea></pre>
</div>
<p>The desktop layout places the search box beside the record counter. Below 900 pixels, it moves to a separate line so the panel heading remains usable on smaller screens.</p>
<p>After rebuilding the project, open the Dashboard page and enter a phrase in one of the search boxes. Press Enter and confirm that only the corresponding grid is filtered. Click the clear button to restore the full grid, then repeat the test after collapsing and expanding the dashboard panel.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/09/01/add-search-boxes-to-individual-dashboard-grids/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Block Compromised Passwords and High-Risk IP Addresses</title>
		<link>https://xlinesoft.com/blog/2026/08/30/block-compromised-passwords-and-high-risk-ip-addresses/</link>
					<comments>https://xlinesoft.com/blog/2026/08/30/block-compromised-passwords-and-high-risk-ip-addresses/#respond</comments>
		
		<dc:creator><![CDATA[Sergey Kornilov]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 15:31:58 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Tutorials]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3474</guid>

					<description><![CDATA[Add compromised-password screening and IP reputation checks to PHPRunner and ASPRunner.NET applications using haveibeenpwned.com, abuseipdb.com, field events, and a local cache.]]></description>
										<content:encoded><![CDATA[<p>PHPRunner and ASPRunner.NET already provide password policies, encrypted password storage, CAPTCHA, two-factor authentication, and user permissions. In this tutorial, we will add two more checks: reject passwords found in known data breaches and block visitors whose IP addresses have a very high abuse score.</p>
<p>During registration, <a href="https://haveibeenpwned.com/Passwords">haveibeenpwned.com</a> checks whether the password has appeared in known data breaches. The password itself is never sent to the service, and this API does not require a key.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/password_compromised.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/password_compromised.png" alt="" width="997" height="576" class="alignnone size-full wp-image-3478" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/password_compromised.png 997w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/password_compromised-600x347.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/password_compromised-768x444.png 768w" sizes="auto, (max-width: 997px) 100vw, 997px" /></a></p>
<p>Before processing an application page, <a href="https://www.abuseipdb.com/">abuseipdb.com</a> checks whether the visitor&#8217;s IP address has been associated with abusive activity. This service requires an API key. Section 6 explains how to create that key. The results are cached locally to reduce the number of external requests.</p>
<p><span id="more-3474"></span></p>
<h3>1. Creating the IP reputation cache table</h3>
<p>The example project uses MySQL. Create the following table in the application&#8217;s database, then add it to the project on the Datasource tables screen.</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="sql" name="mshighlighter" >
CREATE TABLE ip_reputation_cache (
    ip_address VARCHAR(45) NOT NULL,
    abuse_score INT NOT NULL DEFAULT 0,
    total_reports INT NOT NULL DEFAULT 0,
    country_code VARCHAR(2) NULL,
    usage_type VARCHAR(100) NULL,
    checked_at DATETIME NOT NULL,
    PRIMARY KEY (ip_address)
);</textarea></pre>
</div>
<h3>2. Adding the reusable security-check functions</h3>
<p>The first function creates a SHA-1 hash of the password and sends only its first five characters to Pwned Passwords. The remaining hash characters never leave the application.</p>
<p>The second function checks the local cache before contacting AbuseIPDB.</p>
<p><strong>PHPRunner</strong></p>
<p>Open <strong>Style Editor → Custom Files</strong>, create <strong>security_checks.php</strong>, and add this code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
<?php

function pwnedPasswordCount($password)
{
    $hash = strtoupper(sha1($password));
    $prefix = substr($hash, 0, 5);
    $suffix = substr($hash, 5);

    $ch = curl_init(
        "https://api.pwnedpasswords.com/range/" . $prefix
    );

    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_HTTPHEADER => array(
            "User-Agent: PHPRunner-PwnedPasswordCheck",
            "Add-Padding: true"
        )
    ));

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $httpCode != 200) {
        return -1;
    }

    foreach (preg_split('/\r\n|\r|\n/', $response) as $line) {
        $parts = explode(':', trim($line), 2);

        if (
            count($parts) == 2
            && strtoupper($parts[0]) === $suffix
        ) {
            return (int)$parts[1];
        }
    }

    return 0;
}

function ipCheckFailure($message)
{
    return array(
        "success" => false,
        "error" => $message
    );
}

function checkIPReputation($ipAddress, $apiKey)
{
    if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
        return ipCheckFailure("Invalid IP address.");
    }

    $sql = DB::PrepareSQL(
        "SELECT *
         FROM ip_reputation_cache
         WHERE ip_address=':1'
         AND checked_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
        $ipAddress
    );

    $rs = DB::Query($sql);
    $cached = $rs ? $rs->fetchAssoc() : false;

    if ($cached) {
        return array(
            "success" => true,
            "cached" => true,
            "abuseScore" => (int)$cached["abuse_score"],
            "totalReports" => (int)$cached["total_reports"],
            "countryCode" => $cached["country_code"],
            "usageType" => $cached["usage_type"]
        );
    }

    $url = "https://api.abuseipdb.com/api/v2/check?"
        . http_build_query(array(
            "ipAddress" => $ipAddress,
            "maxAgeInDays" => 90
        ));

    $ch = curl_init($url);

    curl_setopt_array($ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_HTTPHEADER => array(
            "Accept: application/json",
            "Key: " . $apiKey
        )
    ));

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $httpCode != 200) {
        return ipCheckFailure("Unable to check IP reputation.");
    }

    $json = json_decode($response, true);

    if (!isset($json["data"])) {
        return ipCheckFailure("Unexpected AbuseIPDB response.");
    }

    $data = $json["data"];
    $abuseScore = (int)($data["abuseConfidenceScore"] ?? 0);
    $totalReports = (int)($data["totalReports"] ?? 0);
    $countryCode = $data["countryCode"] ?? "";
    $usageType = $data["usageType"] ?? "";

    DB::Exec(DB::PrepareSQL(
        "DELETE FROM ip_reputation_cache
         WHERE ip_address=':1'",
        $ipAddress
    ));

    DB::Exec(DB::PrepareSQL(
        "INSERT INTO ip_reputation_cache
         (ip_address, abuse_score, total_reports,
          country_code, usage_type, checked_at)
         VALUES (':1', :2, :3, ':4', ':5', NOW())",
        $ipAddress,
        $abuseScore,
        $totalReports,
        $countryCode,
        $usageType
    ));

    return array(
        "success" => true,
        "cached" => false,
        "abuseScore" => $abuseScore,
        "totalReports" => $totalReports,
        "countryCode" => $countryCode,
        "usageType" => $usageType
    );
}</textarea></pre>
</div>
<p>Open <strong>Events → Global events → After App Init</strong> and load the file:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
require_once("security_checks.php");</textarea></pre>
</div>
<p><strong>ASPRunner.NET</strong></p>
<p>Open <strong>Style Editor → Custom Files</strong>, create <strong>security_checks.cs</strong>, and add this code. ASPRunner.NET compiles C# custom files with the generated application, so no separate include statement is required.</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using runnerDotNet;

namespace runnerDotNet
{
    public partial class CommonFunctions
    {
        public static int PwnedPasswordCount(string password)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(password);
            byte[] digest;

            using (SHA1 sha1 = SHA1.Create())
            {
                digest = sha1.ComputeHash(bytes);
            }

            string hash = BitConverter.ToString(digest)
                .Replace("-", "")
                .ToUpperInvariant();

            string prefix = hash.Substring(0, 5);
            string suffix = hash.Substring(5);

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
                    "https://api.pwnedpasswords.com/range/" + prefix
                );
                request.Method = "GET";
                request.Timeout = 5000;
                request.ReadWriteTimeout = 5000;
                request.UserAgent =
                    "ASPRunner.NET-PwnedPasswordCheck";
                request.Headers["Add-Padding"] = "true";

                using (HttpWebResponse response =
                    (HttpWebResponse)request.GetResponse())
                using (StreamReader reader = new StreamReader(
                    response.GetResponseStream()
                ))
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                        return -1;

                    string body = reader.ReadToEnd();
                    string[] lines = body.Split(
                        new[] { "\r\n", "\n", "\r" },
                        StringSplitOptions.RemoveEmptyEntries
                    );

                    foreach (string line in lines)
                    {
                        string[] parts = line.Trim().Split(':');

                        if (
                            parts.Length == 2
                            && parts[0].ToUpperInvariant() == suffix
                        )
                        {
                            int count;
                            return Int32.TryParse(parts[1], out count)
                                ? count
                                : -1;
                        }
                    }
                }

                return 0;
            }
            catch
            {
                return -1;
            }
        }

        private static XVar IpCheckFailure(string message)
        {
            return new XVar(
                "success", false,
                "error", message
            );
        }

        public static XVar CheckIPReputation(
            string ipAddress,
            string apiKey
        )
        {
            if (String.IsNullOrWhiteSpace(ipAddress))
                return IpCheckFailure("Invalid IP address.");

            dynamic sql = DB.PrepareSQL(
                @"SELECT *
                  FROM ip_reputation_cache
                  WHERE ip_address=':1'
                  AND checked_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
                ipAddress
            );

            dynamic rs = DB.Query(sql);
            dynamic cached = XVar.Pack(rs) ? rs.fetchAssoc() : null;

            if (XVar.Pack(cached))
            {
                return new XVar(
                    "success", true,
                    "cached", true,
                    "abuseScore", cached["abuse_score"],
                    "totalReports", cached["total_reports"],
                    "countryCode", cached["country_code"],
                    "usageType", cached["usage_type"]
                );
            }

            string url =
                "https://api.abuseipdb.com/api/v2/check"
                + "?ipAddress=" + HttpUtility.UrlEncode(ipAddress)
                + "&maxAgeInDays=90";

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.Method = "GET";
                request.Timeout = 5000;
                request.ReadWriteTimeout = 5000;
                request.Accept = "application/json";
                request.Headers["Key"] = apiKey;

                string responseText;

                using (HttpWebResponse response =
                    (HttpWebResponse)request.GetResponse())
                using (StreamReader reader = new StreamReader(
                    response.GetResponseStream()
                ))
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                        return IpCheckFailure(
                            "Unable to check IP reputation."
                        );

                    responseText = reader.ReadToEnd();
                }

                dynamic json = MVCFunctions.runner_json_decode(responseText);

                if (!XVar.Pack(json) || !XVar.Pack(json["data"]))
                    return IpCheckFailure(
                        "Unexpected AbuseIPDB response."
                    );

                dynamic data = json["data"];
                dynamic abuseScore = data["abuseConfidenceScore"];
                dynamic totalReports = data["totalReports"];
                dynamic countryCode = data["countryCode"];
                dynamic usageType = data["usageType"];

                DB.Exec(DB.PrepareSQL(
                    @"DELETE FROM ip_reputation_cache
                      WHERE ip_address=':1'",
                    ipAddress
                ));

                DB.Exec(DB.PrepareSQL(
                    @"INSERT INTO ip_reputation_cache
                      (ip_address, abuse_score, total_reports,
                       country_code, usage_type, checked_at)
                      VALUES (':1', :2, :3, ':4', ':5', NOW())",
                    ipAddress,
                    abuseScore,
                    totalReports,
                    countryCode,
                    usageType
                ));

                return new XVar(
                    "success", true,
                    "cached", false,
                    "abuseScore", abuseScore,
                    "totalReports", totalReports,
                    "countryCode", countryCode,
                    "usageType", usageType
                );
            }
            catch
            {
                return IpCheckFailure(
                    "Unable to check IP reputation."
                );
            }
        }
    }
}</textarea></pre>
</div>
<h3>3. Checking the password from a field event</h3>
<p>Open the Register page in Page Designer. Select the password field, open its field events, and add an event that runs when the user leaves the field.</p>
<p>Use the following JavaScript in <strong>Client Before</strong>:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
pageObj.getItemButton("register_save").addClass("disabled");

params["password"] = this.getValue();

if (!params["password"]) {
    return false;
}</textarea></pre>
</div>
<p>In the <strong>Server</strong> part, add the code for your product.</p>
<p><strong>PHP</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$result["count"] = pwnedPasswordCount($params["password"]);</textarea></pre>
</div>
<p><strong>C#</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
result["count"] = CommonFunctions.PwnedPasswordCount(
    parameters["password"].ToString()
);</textarea></pre>
</div>
<p>Use this JavaScript in <strong>Client After</strong>:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
var count = Number(result.count);

if (count < 0) {
    swal("The password could not be checked. Please try again.");
    return;
}

if (count > 0) {
    swal(
        "This password has appeared in known data breaches "
        + count
        + " times. Please choose another password."
    );
    return;
}

pageObj.getItemButton("register_save").removeClass("disabled");</textarea></pre>
</div>
<h3>4. Disabling the Register button when the page loads</h3>
<p>Open <strong>Events → Global events → Register page → JavaScript OnLoad</strong> and disable the &#8216;Register&#8217; button until the password check succeeds:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
pageObj.getItemButton("register_save").addClass("disabled");</textarea></pre>
</div>
<p><strong>register_save</strong> is the Item ID of the standard Register button.</p>
<h3>5. Enforcing the password check before registration</h3>
<p>The field event improves the user experience, but client-side controls can be bypassed. Repeat the check in <strong>Events → Global events → Register page → Before registration</strong>.</p>
<p><strong>PHP</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$count = pwnedPasswordCount($userdata["password"]);

if ($count < 0) {
    $message = "The password could not be checked. Please try again.";
    return false;
}

if ($count > 0) {
    $message = "This password has appeared in known data breaches. "
        . "Please choose another password.";
    return false;
}

return true;</textarea></pre>
</div>
<p><strong>C#</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
int count = CommonFunctions.PwnedPasswordCount(
    userdata["password"].ToString()
);

if (count < 0)
{
    message = "The password could not be checked. Please try again.";
    return false;
}

if (count > 0)
{
    message = "This password has appeared in known data breaches. "
        + "Please choose another password.";
    return false;
}

return true;</textarea></pre>
</div>
<p>Replace <strong>password</strong> if your login table uses a different password field name. Field names are case-sensitive.</p>
<h3>6. Checking IP reputation in After App Init</h3>
<p>Create a free account at <a href="https://www.abuseipdb.com/register">abuseipdb.com/register</a>. After signing in, open the account dashboard, select &#8216;API Settings&#8217;, and generate an API v2 key.</p>
<p>Add the following code after the existing code in <strong>Events → Global events → After App Init</strong>. Replace the placeholder with your API key and keep the real key out of public project exports and source repositories.</p>
<p><strong>PHP</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$ipAddress = $_SERVER["REMOTE_ADDR"] ?? "";
$apiKey = "YOUR_ABUSEIPDB_API_KEY";

$ipResult = checkIPReputation($ipAddress, $apiKey);

if (
    $ipResult["success"]
    && $ipResult["abuseScore"] >= 90
) {
    http_response_code(403);
    exit("Access denied.");
}</textarea></pre>
</div>
<p><strong>C#</strong></p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
string ipAddress = HttpContext.Current.Request.UserHostAddress;
string apiKey = "YOUR_ABUSEIPDB_API_KEY";

dynamic ipResult = CommonFunctions.CheckIPReputation(
    ipAddress,
    apiKey
);

if (
    XVar.Pack(ipResult["success"])
    && Convert.ToInt32(ipResult["abuseScore"].ToString()) >= 90
)
{
    HttpContext.Current.Response.StatusCode = 403;
    MVCFunctions.Echo("Access denied.");
    MVCFunctions.Exit();
}</textarea></pre>
</div>
<p>The <strong>checkIPReputation()</strong> function reuses a cached result for 24 hours. The sample uses the MySQL <strong>DATE_SUB()</strong> and <strong>NOW()</strong> functions. Adjust that date expression if your project uses another database.</p>
<p>An AbuseIPDB failure does not block the application. A score of 90 or higher does. Adjust the threshold to match your own security requirements.</p>
<p>If the application runs behind Cloudflare, a load balancer, or another reverse proxy, configure the web server to restore the visitor’s original IP address before this code runs. If the application is accessed directly, no additional configuration is required. Do not read X-Forwarded-For or another forwarding header unless requests to the application can come only through a trusted proxy, because these headers can otherwise be forged.</p>
<p>After rebuilding the project, test registration with a known compromised test password and then with a unique password. Confirm that the server-side registration event still rejects a compromised password if JavaScript is disabled. For the IP check, confirm that the first lookup creates a cache row and subsequent page requests reuse it for 24 hours.</p>
<p>The Pwned Passwords implementation follows the service&#8217;s k-anonymity range-search model and uses response padding. See the <a href="https://haveibeenpwned.com/API/v3#PwnedPasswords">Pwned Passwords API documentation</a>. See the <a href="https://docs.abuseipdb.com/#check-endpoint">AbuseIPDB Check endpoint documentation</a> for API-key and response details.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/08/30/block-compromised-passwords-and-high-risk-ip-addresses/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Add User Impersonation to a PHPRunner Application</title>
		<link>https://xlinesoft.com/blog/2026/08/27/how-to-add-user-impersonation-to-a-phprunner-application/</link>
					<comments>https://xlinesoft.com/blog/2026/08/27/how-to-add-user-impersonation-to-a-phprunner-application/#respond</comments>
		
		<dc:creator><![CDATA[Sergey Kornilov]]></dc:creator>
		<pubDate>Thu, 27 Aug 2026 23:36:52 +0000</pubDate>
				<category><![CDATA[CSS]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3465</guid>

					<description><![CDATA[Learn how to let administrators impersonate application users in PHPRunner, display a clear warning banner, and return safely to the original session.]]></description>
										<content:encoded><![CDATA[<p>PHPRunner&#8217;s Security API lets an administrator switch to another application user without knowing that user&#8217;s password. This is useful when an administrator needs to reproduce a problem using the same pages, permissions, and data access as the user.</p>
<p>In this tutorial we will add an impersonation button to the login table, switch accounts with <strong>Security::loginAs()</strong>, display a warning banner while impersonation is active, and provide a link that restores the original administrator session.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner-1024x661.png" alt="" width="810" height="523" class="alignnone size-large wp-image-3468" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner-1024x661.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner-600x387.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner-768x496.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/impersonation_banner.png 1122w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<p>We will cover:</p>
<p>1. Preparing the login table and administrator group<br />
2. Adding an impersonation button to the List page<br />
3. Showing the button only to administrators<br />
4. Switching to the selected user<br />
5. Redirecting after impersonation<br />
6. Adding an impersonation banner<br />
7. Styling the banner<br />
8. Returning to the administrator session</p>
<p><span id="more-3465"></span></p>
<h3>1. Preparing the login table and administrator group</h3>
<p>This tutorial assumes that database authentication and user group permissions are already configured in PHPRunner. The table used for authentication must also be included in the project and have a List page that administrators can open.</p>
<p>The sample project uses <strong>first_name</strong> as the username field. Replace <strong>first_name</strong> in the code below if your login table uses another field.</p>
<p>We will also use <strong>Administrators</strong> as the administrator group name. Replace it with the group name used in your project.</p>
<h3>2. Adding the impersonation button to the List page</h3>
<p>Open the login table&#8217;s List page in Page Designer and switch to Advanced Grid. Select a grid cell, then use <strong>Insert → Custom button → New button</strong>.</p>
<p>Set the button label to &#8216;Login as this user&#8217; and place it inside the grid row. Select the button and set its <strong>Item ID</strong> to:<br />
&#8216;login_as_user&#8217;</p>
<p>The Item ID is important. It is the value used by <strong>hideItem()</strong> in the next step. You can select the button in Page Designer and find its Item ID in the properties panel on the right.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user-1024x580.png" alt="" width="810" height="459" class="alignnone size-large wp-image-3470" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user-1024x580.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user-600x340.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user-768x435.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user-1536x871.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/Login_as_user.png 1708w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<h3>3. Showing the button only to administrators</h3>
<p>Open <strong>Events → login table → List page → After record processed</strong> and add the following PHP code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
if (
    Security::getUserGroup() != $_SESSION["AdminGroupName"]
    || $data["first_name"] == Security::getUserName()
) {
    $pageObject->hideItem("login_as_user", $recordId);
}</textarea></pre>
</div>
<p>The second argument of <strong>hideItem()</strong> is required because the button is located inside a List page grid row. The code hides the button from non-administrators and also hides it on the administrator&#8217;s own record.</p>
<h3>4. Switching to the selected user</h3>
<p>Open the custom button code and select its <strong>Server</strong> tab. Add the following PHP code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
if (Security::getUserGroup() != $_SESSION["AdminGroupName"]) {
    $result["success"] = false;
    return;
}

$record = $button->getCurrentRecord();

$_SESSION["impersonation_admin"] = Security::getUserName();

Security::loginAs($record["first_name"]);

$result["success"] = true;</textarea></pre>
</div>
<p>The group is checked again on the server so that hiding the button is not the only protection. The original administrator username is saved before <strong>Security::loginAs()</strong> replaces the current login session.</p>
<h3>5. Redirecting after impersonation</h3>
<p>In the same custom button, open the <strong>Client After</strong> tab and add this JavaScript code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
if (result["success"]) {
    window.location.href = "menu.php";
}</textarea></pre>
</div>
<p>The redirect reloads the application menu using the impersonated user&#8217;s permissions.</p>
<h3>6. Adding the impersonation banner</h3>
<p>Proceed to <strong>Style Editor → Templates → header.htm</strong>. Add the following code to the common project header:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
<?php
if (
    Security::isLoggedIn()
    &#038;&#038; !empty($_SESSION["impersonation_admin"])
) {
    $adminUsername = htmlspecialchars(
        $_SESSION["impersonation_admin"],
        ENT_QUOTES,
        "UTF-8"
    );

    $currentUsername = htmlspecialchars(
        Security::getUserName(),
        ENT_QUOTES,
        "UTF-8"
    );
?>
<div id="impersonation-banner">
    <span>
        <strong>Impersonation mode:</strong>
        Admin <strong><?php echo $adminUsername; ?></strong>
        is browsing as
        <strong><?php echo $currentUsername; ?></strong>.
    </span>

    <a href="menu.php?return_admin=1">
        Return to admin session
    </a>
</div>

<script>
(function () {
    var banner = document.getElementById("impersonation-banner");

    if (!banner || !document.body) {
        return;
    }

    document.body.insertBefore(banner, document.body.firstChild);
    document.body.classList.add("impersonation-active");
})();
</script>
<?php
}
?></textarea></pre>
</div>
<p>The banner appears on every generated page while an administrator is impersonating another logged-in user. The usernames are escaped before they are inserted into the page.</p>
<h3>7. Styling the banner</h3>
<p>Open <strong>Style Editor → Modify CSS</strong> and add the following project-level Custom CSS:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
:root {
    --impersonation-banner-height: 40px;
}

#impersonation-banner {
    position: sticky !important;
    top: 0 !important;
    z-index: 1000000 !important;
    width: 100% !important;
    max-width: none !important;
    height: var(--impersonation-banner-height);
    box-sizing: border-box;
    margin: 0 !important;
    padding: 0 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 18px;
    background: #fff3cd;
    border-bottom: 1px solid #e0bd64;
    color: #664d03;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    white-space: nowrap;
}

#impersonation-banner a {
    color: #664d03;
    font-weight: 600;
    text-decoration: underline;
}

body.impersonation-active .r-topheader[data-fixed] {
    top: var(--impersonation-banner-height) !important;
}

body.impersonation-active .r-left[data-fixed] {
    top: var(--impersonation-banner-height) !important;
    height: calc(100vh - var(--impersonation-banner-height)) !important;
}

body.impersonation-active > .r-vbar-page,
body.impersonation-active > .r-topbar-page {
    min-height: calc(100vh - var(--impersonation-banner-height));
}

@media print {
    #impersonation-banner {
        display: none !important;
    }

    body.impersonation-active .r-topheader[data-fixed],
    body.impersonation-active .r-left[data-fixed] {
        top: 0 !important;
    }
}</textarea></pre>
</div>
<p>Besides styling the warning, this CSS moves PHPRunner&#8217;s fixed navigation elements below the banner.</p>
<h3>8. Returning to the administrator session</h3>
<p>Open <strong>Events → Global events → After App Init</strong> and add the following PHP code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$_SESSION["AdminGroupName"] = "Administrators";

if (
    isset($_GET["return_admin"])
    && !empty($_SESSION["impersonation_admin"])
) {
    $adminUsername = $_SESSION["impersonation_admin"];

    unset($_SESSION["impersonation_admin"]);

    Security::loginAs($adminUsername);

    header("Location: menu.php");
    exit();
}</textarea></pre>
</div>
<p>The event defines the administrator group used by the other events. When the return link is clicked, it restores the saved administrator username, removes the impersonation marker, and redirects back to the menu.</p>
<p>After rebuilding the project, log in as an administrator and open the login table&#8217;s List page. Click &#8216;Login as this user&#8217; beside another account. PHPRunner will apply that user&#8217;s identity and permissions, display the impersonation banner, and allow you to return to the original administrator session.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/08/27/how-to-add-user-impersonation-to-a-phprunner-application/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>8 Famous Excel Errors That Cost Millions (and One That Renamed Human Genes)</title>
		<link>https://xlinesoft.com/blog/2026/08/12/8-famous-excel-errors-that-cost-millions-and-one-that-renamed-human-genes/</link>
					<comments>https://xlinesoft.com/blog/2026/08/12/8-famous-excel-errors-that-cost-millions-and-one-that-renamed-human-genes/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 12 Aug 2026 22:25:24 +0000</pubDate>
				<category><![CDATA[Excel]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3416</guid>

					<description><![CDATA[Excel is the most quietly powerful piece of software in most offices. It runs budgets, trading desks, and scientific research, usually without anyone checking the formulas too closely. That is exactly the problem. Below are eight cases where a single dragged cell, a missing minus sign, or a stray autocorrect changed the outcome of something much bigger than a spreadsheet. 1. Excel kept turning gene names into dates, so scientists renamed the genes Excel has a feature that auto-converts anything that looks date-like, such as...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2026/08/12/8-famous-excel-errors-that-cost-millions-and-one-that-renamed-human-genes/" class="more-link">Continue Reading <span class="screen-reader-text">"8 Famous Excel Errors That Cost Millions (and One That Renamed Human Genes)"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Excel is the most quietly powerful piece of software in most offices. It runs budgets, trading desks, and scientific research, usually without anyone checking the formulas too closely.</p>
<p>That is exactly the problem. Below are eight cases where a single dragged cell, a missing minus sign, or a stray autocorrect changed the outcome of something much bigger than a spreadsheet.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM-1024x576.png" alt="" width="810" height="456" class="alignnone size-large wp-image-3421" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM-1024x576.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM-600x338.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM-768x432.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM-1536x864.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/ChatGPT-Image-Aug-12-2026-07_53_39-PM.png 1672w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<p><!--


<div style="background:#f4f8fc; border-left:4px solid #2f80c9; padding:16px 20px; margin:0 0 26px 0; border-radius:4px;">
	

<div style="font-weight:600; margin-bottom:6px;">When Excel becomes a business system</div>



	

<div>
		Not every spreadsheet should stay a spreadsheet forever. When a file starts acting as a shared database or the backbone of a business process, tools like <a href="https://xlinesoft.com/phprunner/index.htm" style="font-weight:600;">PHPRunner</a> can turn that data into a proper multi-user web application, with validation and access control built in, instead of a single file that one wrong paste can quietly break.
	</div>


</div>


--></p>
<p><span id="more-3416"></span></p>
<h3>1. Excel kept turning gene names into dates, so scientists renamed the genes</h3>
<p>Excel has a feature that auto-converts anything that looks date-like, such as &#8220;1-Mar&#8221; or &#8220;Sept 1&#8221;, into an actual date. Convenient for expense reports. Less convenient for genetics.</p>
<p>Human genes have short alphanumeric names, and a number of them look exactly like dates to Excel: MARCH1, SEPT1, DEC1. For years, when researchers uploaded gene lists into spreadsheets, Excel quietly rewrote them into calendar dates. The mistake was invisible unless someone thought to check, and often nobody did before the data went into a published paper.</p>
<p>Eventually the problem became widespread enough that the official gene-naming authority renamed the genes instead of fighting Excel. MARCH1 is now MARCHF1. SEPT1 is now SEPTIN1.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.theverge.com/2020/8/6/21355674/human-genes-rename-microsoft-excel-misreading-dates">The Verge</a></p>
<h3>2. MI5 collected data on 134 wrong phone numbers after a spreadsheet error</h3>
<p>The UK&#8217;s domestic intelligence agency once requested subscriber information for telephone numbers connected to its investigations. A formatting error in a spreadsheet changed the final three digits of a set of numbers to &#8220;000&#8221;.</p>
<p>As a result, MI5 wrongly collected subscriber data relating to 134 telephone numbers that had no connection to its investigations. The material was later destroyed and the formatting fault was fixed.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.theguardian.com/government-computing-network/2011/jul/01/mi5-data-collection-errors">The Guardian</a></p>
<h3>3. Kodak overstated a severance accrual by $11 million because of extra zeros</h3>
<p>In 2005, Kodak overstated a severance accrual by <strong>$11 million</strong>. The cause was a spreadsheet error: according to a company spokesman, too many zeros had been added to the calculation.</p>
<p>The timing made it worse. Kodak was already reporting heavy losses, and the correction landed as one more difficult headline for a company already under pressure.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.marketwatch.com/story/kodak-restates-earnings-adds-9-million-to-latest-loss">MarketWatch</a></p>
<h3>4. A cut-and-paste error cost TransAlta $24 million</h3>
<p>In 2003, TransAlta, a Canadian power company, was submitting bids for US power transmission hedging contracts. During the final sorting and ranking of the bids, a cut-and-paste error in an Excel spreadsheet went undetected.</p>
<p>The mistake caused TransAlta to buy more transmission contracts, and at higher prices, than it should have. The cost: about <strong>$24 million</strong>.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.theregister.com/2003/06/19/excel_snafu_costs_firm_24m/">The Register</a></p>
<h3>5. A missing minus sign turned a $1.3B loss into a $1.3B gain</h3>
<p>In 1994, a Fidelity accountant left the minus sign off a $1.3 billion loss while performing a tax calculation. A loss became a gain, and the total estimate was off by $2.6 billion.</p>
<p>Fidelity had already told shareholders to expect a specific year-end distribution based on the incorrect number, and had to walk it back publicly once the error was caught.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.washingtonpost.com/archive/politics/1995/01/05/an-accountants-nightmare-13-billion-plus-or-minus/0b072038-612e-4a85-8889-57db77434d03/">The Washington Post</a></p>
<h3>6. Barclays learned the hard way that hidden rows aren&#8217;t deleted rows</h3>
<p>During the 2008 financial crisis, Barclays moved quickly to buy Lehman Brothers&#8217; assets out of bankruptcy. The Excel file documenting the deal, containing 1,000 rows and 24,000 cells, was sent to lawyers just hours before a deadline.</p>
<p>Some contracts Barclays did not want had been marked as hidden in the spreadsheet rather than removed. During the process of converting and reformatting the spreadsheet, that distinction was lost, and 179 additional contracts were mistakenly included in the purchase agreement. After the deal was approved, Barclays had to seek legal relief to remove them.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://www.theregister.com/2008/10/15/lehman_excel_snafu_could_cost_barclays_dear/">The Register</a></p>
<h3>7. SUM instead of AVERAGE: the Excel error that understated JPMorgan&#8217;s risk</h3>
<p>Inside JPMorgan&#8217;s 2012 &#8220;London Whale&#8221; trading debacle was a risk model built in Excel. One calculation subtracted an old rate from a new rate and then divided by their <em>sum</em> instead of their <em>average</em>, as the modeler had intended. JPMorgan&#8217;s investigation concluded that the error likely muted measured volatility and lowered the bank&#8217;s Value at Risk estimate.</p>
<p>The spreadsheet error did not by itself cause JPMorgan&#8217;s $6.2 billion trading loss. The bank&#8217;s investigations found much broader failures in trading strategy, oversight and risk controls. But the faulty formula was one reason the model made the portfolio&#8217;s risk look smaller than it really was.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://elischolar.library.yale.edu/cgi/viewcontent.cgi?article=1502&amp;context=ypfs-documents">JPMorgan Management Task Force report</a></p>
<h3>8. A grad student found an Excel error in one of austerity&#8217;s most-cited papers</h3>
<p>This one had the widest reach. In 2010, Harvard economists Carmen Reinhart and Kenneth Rogoff published research reporting that countries with government debt above 90% of GDP averaged roughly -0.1% economic growth. The finding became highly influential in public debates over government debt and austerity and was cited by policymakers in the US and Europe.</p>
<p>Three years later, graduate student Thomas Herndon, working with economists Michael Ash and Robert Pollin, tried to replicate the results using the original spreadsheet. They found an Excel formula that did not extend far enough down the column, silently excluding five countries: Australia, Austria, Belgium, Canada and Denmark.</p>
<p>But the spreadsheet range was not the only problem. Herndon, Ash and Pollin also identified selective exclusion of available data and unconventional weighting of country results. After correcting the problems, they calculated average growth for countries above the 90% debt threshold at <strong>2.2%</strong>, rather than -0.1%.</p>
<p style="font-size:13px; color:#777; margin-top:-8px;">Source: <a href="https://peri.umass.edu/publication/does-high-public-debt-consistently-stifle-economic-growth-a-critique-of-reinhart-and-rogoff/">Herndon, Ash &amp; Pollin, Political Economy Research Institute</a></p>
<h3>The pattern</h3>
<p>None of these are really stories about Excel. They are stories about what happens when a single spreadsheet, unaudited and trusted by default, sits upstream of a decision big enough to matter. A formula, a copy and paste, a dragged range. Nobody meant for it to go this far. It just did, because nobody checked.</p>
<p>If a spreadsheet in your own organization has quietly become a critical system, it may be worth looking at whether it belongs in a proper web application instead. <a href="https://xlinesoft.com/phprunner/index.htm">PHPRunner</a> can import that data into a real database and generate a multi-user application around it, complete with permissions and an audit trail.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/08/12/8-famous-excel-errors-that-cost-millions-and-one-that-renamed-human-genes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introducing Harbor: A Fresh New Theme for PHPRunner/ASPRunner.NET</title>
		<link>https://xlinesoft.com/blog/2026/08/06/introducing-harbor-a-fresh-new-theme-for-phprunner-asprunner-net/</link>
					<comments>https://xlinesoft.com/blog/2026/08/06/introducing-harbor-a-fresh-new-theme-for-phprunner-asprunner-net/#comments</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 15:45:02 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3407</guid>

					<description><![CDATA[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...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2026/08/06/introducing-harbor-a-fresh-new-theme-for-phprunner-asprunner-net/" class="more-link">Continue Reading <span class="screen-reader-text">"Introducing Harbor: A Fresh New Theme for PHPRunner/ASPRunner.NET"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Version 11.3 build 44452 introduces new visual theme: Harbor.</p>
<p>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.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list-1024x550.png" alt="" width="810" height="435" class="alignnone size-large wp-image-3411" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list-1024x550.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list-600x323.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list-768x413.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list-1536x826.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_list.png 1920w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a><br />
<span id="more-3407"></span></p>
<p><strong>Key features:</strong></p>
<ul>
<li>Professional navy, white, gray, and blue color palette.</li>
<li>Distinctive dark sidebar with clear active and hover states.</li>
<li>Cleaner grids with horizontal separators and no vertical lines.</li>
<li>Improved table spacing, row highlighting, and numeric alignment.</li>
<li>Consistent card styling across panels, forms, and dashboards.</li>
<li>Unified dashboard widgets, charts, snippets, and grid elements.</li>
<li>Cleaner Add, Edit, and View pages with spacious fields.</li>
<li>Modern tabs with simple blue underline navigation.</li>
<li>Refined buttons with icons, hover, focus, and disabled states.</li>
<li>Polished dropdowns, pagination, breadcrumbs, alerts, and modals.</li>
</ul>
<p><strong>Additional considerations:</strong></p>
<p>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. </p>
<ul>
<li>Do not overcrowd your pages with data. If you display thirty fields in a grid, it is not going to look well. Choose only those fields that are necessary and mov the rest to additional pages like View.</li>
<li>Turn off vertical lines in grids. It brings focus to the actual data</li>
<li>Add icons to some common buttons like &#8216;Add new&#8217; and &#8216;Back to list&#8217;. Not every button needs it, use with moderation.</li>
</ul>
<p><strong>More screenshots:</strong></p>
<p>Edit page:</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit-1024x550.png" alt="" width="810" height="435" class="alignnone size-large wp-image-3410" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit-1024x550.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit-600x323.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit-768x413.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit-1536x826.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_edit.png 1920w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<p>Dashboard: </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard-1024x550.png" alt="" width="810" height="435" class="alignnone size-large wp-image-3409" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard-1024x550.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard-600x323.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard-768x413.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard-1536x826.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_dashboard.png 1920w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<p>Welcome page: </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome-1024x563.png" alt="" width="810" height="445" class="alignnone size-large wp-image-3412" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome-1024x563.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome-600x330.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome-768x422.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome-1536x845.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2026/08/harbor_welcome.png 1864w" sizes="auto, (max-width: 810px) 100vw, 810px" /></a></p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/08/06/introducing-harbor-a-fresh-new-theme-for-phprunner-asprunner-net/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Building a World Cup Prediction Challenge in PHPRunner</title>
		<link>https://xlinesoft.com/blog/2026/07/21/building-a-world-cup-prediction-challenge-in-phprunner/</link>
					<comments>https://xlinesoft.com/blog/2026/07/21/building-a-world-cup-prediction-challenge-in-phprunner/#comments</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 20:36:19 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Tutorials]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3379</guid>

					<description><![CDATA[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: 1. Creating image-based...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2026/07/21/building-a-world-cup-prediction-challenge-in-phprunner/" class="more-link">Continue Reading <span class="screen-reader-text">"Building a World Cup Prediction Challenge in PHPRunner"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>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!</p>
<p>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. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_leaderboard.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_leaderboard.png" alt="" width="900" height="505" class="alignnone size-full wp-image-3383" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_leaderboard.png 900w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_leaderboard-600x337.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_leaderboard-768x431.png 768w" sizes="auto, (max-width: 900px) 100vw, 900px" /></a></p>
<p><a href="https://xlinesoft.com/projects/worldcup2026/leaderboard_list.php"><i class="fas fa-link"></i> See the final leaderboard online</a></p>
<p>We will talk about:</p>
<p>1. Creating image-based edit controls<br />
2. Grouping team pickers and preventing duplicate selections<br />
3. Enforcing one prediction per user with PHPRunner events<br />
4. Disabling Add and Edit after the submission deadline<br />
5. Enabling Save only after all required answers are selected<br />
6. Calculating scores and rankings with custom SQL<br />
7. Adding rank graphics, progress bars, and current-user highlighting<br />
8. Integrating custom landing, scoring, and thank-you pages<br />
9. A few more visual tweaks<br />
10. How to create your own custom Edit controls easily</p>
<p><span id="more-3379"></span></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/WorldCup2026Codex_20260721215446607.zip"><i class="fas fa-download"></i> Download project for PHPRunner</a></p>
<h3>1. Creating image-based edit controls with table and SQL lookups</h3>
<p>I think this is the most important part. We introduced three image-based custom edit controls that you can use in other applications as well. All three are available for download on our marketplace. Right now it is for PHPRunner only but we will provide ASPRunner.NET equivalents very soon. We will also teach you how to create new custom Edit controls using ChatGPT. </p>
<p><strong>ImagePicker</strong></p>
<p><a href="https://xlinesoft.com/marketplace/ImagePicker"><i class="fas fa-link"></i> ImagePicker on marketplace</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker.png" alt="" width="1302" height="854" class="alignnone size-full wp-image-3385" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker.png 1302w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker-600x394.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker-1024x672.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagepicker-768x504.png 768w" sizes="auto, (max-width: 1302px) 100vw, 1302px" /></a></p>
<p>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. </p>
<p><strong>ImageDropdown</strong></p>
<p><a href="https://xlinesoft.com/marketplace/ImageDropdown"><i class="fas fa-link"></i> ImageDropdown on marketplace</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown.png" alt="" width="1241" height="612" class="alignnone size-full wp-image-3387" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown.png 1241w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown-600x296.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown-1024x505.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagedropdown-768x379.png 768w" sizes="auto, (max-width: 1241px) 100vw, 1241px" /></a></p>
<p>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.  </p>
<p><strong>ImageChoice</strong></p>
<p><a href="https://xlinesoft.com/marketplace/ImageChoice"><i class="fas fa-link"></i> ImageChoice on marketplace</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagechoice-2.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagechoice-2.png" alt="" width="1024" height="375" class="alignnone size-full wp-image-3386" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagechoice-2.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagechoice-2-600x220.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/imagechoice-2-768x281.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></p>
<p>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.  </p>
<h3>2. Grouping team pickers and preventing duplicate selections</h3>
<p>The Top 8 prediction is stored in eight fields: top8_team1_id through top8_team8_id. Without additional logic, eight independent lookup controls would allow the same country to be selected several times. The project solves this inside the custom edit control by assigning all eight controls to one group.</p>
<p>Each Top 8 field uses the following group settings:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$this->settings["picker_group"] = "top8";
$this->settings["max_group_count"] = 8;
$this->settings["status_element_id"] = ["status_element_id"];
$this->settings["prevent_duplicates"] = 1;</textarea></pre>
</div>
<p>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 (&#8220;status_element_id&#8221;) can report progress across the whole group rather than treating each field as an unrelated input. You can see status field saying something like &#8220;7 of 8 selected&#8221; at the top right corner of the first section. </p>
<p>The podium fields use the same idea with a different group:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$this->settings["picker_group"] = "podium";
$this->settings["prevent_duplicates"] = 1;</textarea></pre>
</div>
<p>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.</p>
<h3>3. Enforcing one prediction per user with PHPRunner events</h3>
<p>Every participant should have one prediction record. Hiding the Add button after submission is not sufficient because the user could revisit the Add URL directly. The project checks for an existing record in the prediction table&#8217;s Before Process Add event.</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$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();
}</textarea></pre>
</div>
<p>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.</p>
<p>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.</p>
<h3>4. Disabling Add and Edit after the submission deadline</h3>
<p>Prediction forms must close at a known time. Instead of only hiding buttons, the project changes the table permission string in the Table permissions event. This affects PHPRunner&#8217;s permission checks for every request. The following code was added to wc_predictions table GetTablePermissions event. </p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$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;</textarea></pre>
</div>
<p>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.</p>
<p>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.</p>
<p>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&#8217;s prediction ID before redirecting to View so the destination contains a valid editid1 key.</p>
<h3>5. Enabling &#8216;Save&#8217; button only after all required answers are selected</h3>
<p>PHPRunner&#8217;s normal validation works field by field. This form also needs a page-level rule: the participant should not submit until every required prediction category is complete. The Add and Edit JavaScript OnLoad events monitor the required controls and toggle the Save item.</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
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());
}</textarea></pre>
</div>
<p>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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="javascript" name="mshighlighter" >
$(document).on(
    "change keyup click",
    "input, select, textarea, .ip-control, .idrop-control, .ichoice-card",
    function() {
        setTimeout(refreshSaveButton, 50);
    }
);</textarea></pre>
</div>
<p>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=&#8221;0&#8243; and max=&#8221;10&#8243; to the final-score inputs.</p>
<p>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.</p>
<h3>6. Calculating scores and rankings with custom SQL</h3>
<p>The Leaderboard is implemented as a custom view. We wanted to perform the whole score calculation in SQL query so it looks quite tricky. </p>
<p>Top 8 selections are unordered. Each predicted team is tested against all eight actual Top 8 fields:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="sql" name="mshighlighter" >
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 0
END</textarea></pre>
</div>
<p>NULLIF(&#8230;, 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.</p>
<p>The exact-position categories use direct comparisons with their own weights:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="sql" name="mshighlighter" >
CASE
    WHEN a.champion_team_id > 0
     AND p.champion_team_id = a.champion_team_id
    THEN 25 ELSE 0
END
+
CASE
    WHEN a.second_place_team_id > 0
     AND p.second_place_team_id = a.second_place_team_id
    THEN 15 ELSE 0
END
+
CASE
    WHEN a.third_place_team_id > 0
     AND p.third_place_team_id = a.third_place_team_id
    THEN 10 ELSE 0
END</textarea></pre>
</div>
<p>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.</p>
<p>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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="sql" name="mshighlighter" >
ORDER BY points DESC, correct_picks DESC, submitted_at ASC</textarea></pre>
</div>
<h3>7. Adding rank graphics, progress bars, and current-user highlighting</h3>
<p>The 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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$_SESSION["rank"] = 1;
unset(
    $_SESSION["leaderboard_current_rank"],
    $_SESSION["leaderboard_current_points"],
    $_SESSION["leaderboard_current_player"]
);</textarea></pre>
</div>
<p>Before each row is displayed, the project compares the row&#8217;s player with the current user&#8217;s display name. When they match, it stores the rank and points for the standing panel and highlights the row:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$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"]++;</textarea></pre>
</div>
<p>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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$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>';</textarea></pre>
</div>
<p>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.</p>
<p>For a larger application, compare users by an immutable user ID rather than display_name. Display names can change and may not be unique.</p>
<h3>8. Integrating custom landing, scoring, and thank-you pages</h3>
<p>Not every page in a PHPRunner application must be generated from a database table. This project carries standalone files in the export&#8217;s files/ directory. PHPRunner copies them into the output folder when the application is built.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_index.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_index.png" alt="" width="900" height="517" class="alignnone size-full wp-image-3392" srcset="https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_index.png 900w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_index-600x345.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2026/07/world_cup_index-768x441.png 768w" sizes="auto, (max-width: 900px) 100vw, 900px" /></a></p>
<p>index.php is the public homepage/landing page. It explains the challenge, dates, prizes, and entry process, then links visitors to PHPRunner&#8217;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.</p>
<p>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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
header("Location: thankyou.php");
exit();</textarea></pre>
</div>
<p>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:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
// Before Register
$userdata["usertype"] = "user";
return true;

// After Register
Security::loginAs($userdata["email"]);
header("Location: wc_predictions_add.php");
exit();</textarea></pre>
</div>
<p>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.</p>
<p>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.</p>
<h3>9. A few more visual tweaks</h3>
<p><strong>Yes/No switch on Add/Edit pages</strong></p>
<p>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 &#8216;Final goes to penalty?&#8217; 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. </p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
$(function() {
    $('input[name^="radio_final_goes_to_penalties_"]')
        .closest('.rnr-horizontal-lookup')
        .addClass('wc-penalty-toggle');
});</textarea></pre>
</div>
<p>Check classes that start with <strong>.wc-penalty-toggle</strong> in Custom CSS.</p>
<p>One more tweak, the display of <strong>correct_picks</strong> field on leaderboard page. It is implemented as &#8216;View as&#8217; Custom and use some additional CSS classes. Here is the code:</p>
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="php" name="mshighlighter" >
$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>';</textarea></pre>
</div>
<p>In this code 16 is the total number of picks and this is how we calculate the percentage of correct picks. </p>
<h3>10. How to create your own custom Edit controls easily</h3>
<ul>
<li><a href="https://xlinesoft.com/blog/wp-content/uploads/2026/07/phprunner_custom_edit_controls_skill_pack.zip"><i class="fas fa-link"></i> Download this skill pack</a></li>
<li>Start a new ChatGPT conversation</li>
<li>Upload the ZIP</li>
<li>Tell ChatGPT: &#8220;Use the PHPRunner custom Edit controls skill from the uploaded ZIP.&#8221;</li>
<li>Then describe the control you want. For instance:
<div class="my-syntax-highlighter">
<pre><textarea id="mshighlighter" class="mshighlighter" language="" name="mshighlighter" >
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.</textarea></pre>
</div>
</li>
</ul>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2026/07/21/building-a-world-cup-prediction-challenge-in-phprunner/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Version 11.3</title>
		<link>https://xlinesoft.com/blog/2025/12/24/version-11-3/</link>
					<comments>https://xlinesoft.com/blog/2025/12/24/version-11-3/#comments</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 24 Dec 2025 16:20:46 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3330</guid>

					<description><![CDATA[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 1. AI features. Help with writing the code ( PHP, C#, Javascript, CSS, SQL ). More info in the manual Video on AI features from Corrie 2. Intellisense. Code completion in all areas where code can be...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2025/12/24/version-11-3/" class="more-link">Continue Reading <span class="screen-reader-text">"Version 11.3"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p><strong>Version 11.3 release is here!</strong></p>
<p>Find trial version download links below. If you are a customer with a valid maintenance, you can download registered version 11.3 via your <span style="width: 300px; display: inline-block;"><a style="color: white;" class="btn_purple" href="https://xlinesoft.com/dss/support.php">control panel</a></span>. </p>
<p>You can watch <a href="https://youtu.be/5fX1vV7Z_nw">this video on YouTube</a> that explains how to use new features. </p>
<p><span style="width: 300px; display: inline-block;"><a style="color: white;" class="btn_purple" href="https://xlinesoft.com/download">Trial version download links</a></span></p>
<p>1. AI features. Help with writing the code ( PHP, C#, Javascript, CSS, SQL ).<br />
<a href="https://xlinesoft.com/phprunner/docs/cody_ai_assistant.htm">More info in the manual</a><br />
<a href="https://www.youtube.com/watch?v=bKS9y_7d8uM">Video on AI features from Corrie</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant-600x333.png" alt="" width="600" height="333" class="alignnone size-medium wp-image-3365" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant-600x333.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant-1024x568.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant-768x426.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant-1536x853.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/event_editor_ai_assistant.png 1598w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a><br />
<span id="more-3330"></span></p>
<p>2. Intellisense. Code completion in all areas where code can be edited.<br />
<a href="https://xlinesoft.com/phprunner/docs/events.htm#intellisense">More info</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/intellisense_dal.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/intellisense_dal-600x389.png" alt="" width="600" height="389" class="alignnone size-medium wp-image-3364" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/intellisense_dal-600x389.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/intellisense_dal-768x498.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/intellisense_dal.png 896w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p>3. Save project as template<br />
<a href="https://xlinesoft.com/phprunner/docs/working_with_projects.htm#save_as_template">More info</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-600x332.png" alt="" width="600" height="332" class="alignnone size-medium wp-image-3362" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-600x332.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-768x425.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template.png 901w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p>4. Export/import of individual objects between projects. We you export a table in includes everything that is relevant: field settings, page designs, events etc.<br />
<a href="https://xlinesoft.com/phprunner/docs/datasource_tables_screen.htm#exportimport">More info</a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/import_table_create.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/import_table_create-600x356.png" alt="" width="600" height="356" class="alignnone size-medium wp-image-3363" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/import_table_create-600x356.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/import_table_create-768x455.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/import_table_create.png 899w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p>5. 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. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-600x332.png" alt="" width="600" height="332" class="alignnone size-medium wp-image-3362" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-600x332.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template-768x425.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/save_as_template.png 901w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p>6. New cloud providers: Google Cloud and Azure Blob Storage<br />
<a href="https://xlinesoft.com/phprunner/docs/google-cloud-storage.htm">Google Cloud Storage info</a><br />
<a href="https://xlinesoft.com/phprunner/docs/azure-blob-storage.htm">Azure Blob Storage info</a></p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2025/12/24/version-11-3/feed/</wfw:commentRss>
			<slash:comments>12</slash:comments>
		
		
			</item>
		<item>
		<title>Version 11.2</title>
		<link>https://xlinesoft.com/blog/2025/12/10/version-11-2/</link>
					<comments>https://xlinesoft.com/blog/2025/12/10/version-11-2/#comments</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 15:50:13 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://xlinesoft.com/blog/?p=3236</guid>

					<description><![CDATA[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 &#8216;My purchases&#8217;. All others can find trial version links below. Trial version download links PHPRunner 11.2 for Windows trial &#160;&#160; 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...<span class="clearfix clearfix-post"></span><a href="https://xlinesoft.com/blog/2025/12/10/version-11-2/" class="more-link">Continue Reading <span class="screen-reader-text">"Version 11.2"</span> <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>The final version of PHPRunner/ASPRunner.NET 11.2 is here! To download a registered version logon to your <a href='https://xlinesoft.com/dss/support.php'>control panel</a> account and find download links under &#8216;My purchases&#8217;. </p>
<p>All others can find trial version links below. </p>
<h3>Trial version download links</h3>
<p><a href="https://asprunner.com/files/phprunner-trial.exe" class="btn_purple" style="color: white;" contenteditable="false" style="cursor: pointer;">PHPRunner 11.2 for Windows trial</a><br />
&nbsp;&nbsp;<br />
<a href="https://asprunner.com/files/asprunnernet-trial.exe" class="btn_purple" style="color: white;" contenteditable="false" style="cursor: pointer;">ASPRunner.NET 11.2 for Windows trial</a></p>
<p><a href="https://asprunner.com/files/phprunner-trial.dmg" class="btn_purple" style="color: white;" contenteditable="false" style="cursor: pointer;">PHPRunner 11.2 for Mac trial</a></p>
<p><a href="https://asprunner.com/files/phprunner-trial.deb" class="btn_purple" style="color: white;" contenteditable="false" style="cursor: pointer;">PHPRunner 11.2 for Linux (Debian) trial</a></p>
<p><a href="https://asprunner.com/files/phprunner-trial.rpm" class="btn_purple" style="color: white;" contenteditable="false" style="cursor: pointer;">PHPRunner 11.2 for Linux (RedHat) trial</a></p>
<p>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. </p>
<p>This new version comes with ten improvements. </p>
<h3>1. Cards layout on the List page</h3>
<p>Similar 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. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web-600x362.png" alt="" width="600" height="362" class="alignnone size-medium wp-image-3284" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web-600x362.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web-1024x618.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web-768x463.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web-1536x927.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2025/10/cards_web.png 1667w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a><br />
<span id="more-3236"></span></p>
<h3>2. Two new visual themes</h3>
<p>New visual themes are named Futuro (light) and Obsidian (dark). You can choose them on the Style Editor screen. They are modern, sleek and lightweight. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/light.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/light-600x325.png" alt="" width="600" height="325" class="alignnone size-medium wp-image-3311" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/light-600x325.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/light-1024x555.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/light-768x416.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/light-1536x832.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/light.png 1920w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark-600x323.png" alt="" width="600" height="323" class="alignnone size-medium wp-image-3309" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark-600x323.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark-1024x550.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark-768x413.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark-1536x826.png 1536w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dark.png 1920w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>3. Three new welcome page layouts</h3>
<p>New 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. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome-600x376.png" alt="" width="600" height="376" class="alignnone size-medium wp-image-3310" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome-600x376.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome-1024x641.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome-768x481.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/welcome.png 1462w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>4. Dashboards tabs</h3>
<p>You can combine several dashboard elements into a single one and use tabs to switch between them. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dashboard_tabs.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dashboard_tabs-600x402.png" alt="" width="600" height="402" class="alignnone size-medium wp-image-3308" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/dashboard_tabs-600x402.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dashboard_tabs-768x515.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/dashboard_tabs.png 786w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>5. Dashboard widgets</h3>
<p>Similar to dashboard code snippets but easier to customize. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/widgets.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/widgets-600x128.png" alt="" width="600" height="128" class="alignnone size-medium wp-image-3316" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/widgets-600x128.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/widgets-768x164.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/widgets.png 853w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>6. SMTP with OAuth support</h3>
<p>At this moment we support Gmail Workplace and Microsoft Exchange accounts.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/oauth.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/oauth-600x456.png" alt="" width="600" height="456" class="alignnone size-medium wp-image-3326" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/oauth-600x456.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/oauth-768x583.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/oauth.png 828w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>7. &#8220;Javascript Create TinyMCE&#8221; event for TinyMCE customization</h3>
<p>This new event allows you to customize TinyMCE editor behavior across the whole project. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/tinymce.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/tinymce-600x309.png" alt="" width="600" height="309" class="alignnone size-medium wp-image-3324" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/tinymce-600x309.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/tinymce-768x396.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/tinymce.png 896w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>8. ‘View as’ option Copy value to clipboard</h3>
<p>This option can be enabled for any text field under &#8216;View as&#8217; settings.</p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/clipboard.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/clipboard-600x88.png" alt="" width="600" height="88" class="alignnone size-medium wp-image-3320" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/clipboard-600x88.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/clipboard.png 603w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>9. Foldable elements in dashboards</h3>
<p>All dashboard elements can be folded by clicking on their header. </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable-600x376.png" alt="" width="600" height="376" class="alignnone size-medium wp-image-3318" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable-600x376.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable-1024x641.png 1024w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable-768x481.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/foldable.png 1462w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<h3>10. Show details key column as a hyperlink</h3>
<p>Hyperlink 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 &#8216;Order Details&#8217; table. It is set to open master table data in a popup.  </p>
<p><a href="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foreign.png"><img loading="lazy" decoding="async" src="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foreign-600x480.png" alt="" width="600" height="480" class="alignnone size-medium wp-image-3322" srcset="https://xlinesoft.com/blog/wp-content/uploads/2025/12/foreign-600x480.png 600w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/foreign-768x614.png 768w, https://xlinesoft.com/blog/wp-content/uploads/2025/12/foreign.png 1002w" sizes="auto, (max-width: 600px) 100vw, 600px" /></a></p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://xlinesoft.com/blog/2025/12/10/version-11-2/feed/</wfw:commentRss>
			<slash:comments>14</slash:comments>
		
		
			</item>
	</channel>
</rss>
