Your Complete Guide to Low-Code Development
Fractal Studio is a powerful low-code development platform that combines the flexibility of traditional programming with the speed of visual development. It uses a unique approach where:
Like database tables, but stored as JSON files. Each collection can have multiple documents.
Configuration layers that control UI, validation, events, and more without changing code.
HTML templates that define how your data is displayed to users.
C# methods that respond to user actions like button clicks or form submissions.
When you open Fractal Studio, you'll start with creating a new project:
// The system generates a unique name like "HelloWorld_a1b2c3d4" var appName = "HelloWorld_a1b2c3d4"; // Project structure is automatically created Projects/ FractalPlatform.HelloWorld_a1b2c3d4/ HelloWorldApplication.cs // Main code file Database/ // JSON data storage Layouts/ // HTML templates Files/ // Static resources
Fractal Studio's main interface has three sections:
| Section | Purpose | What You'll Do |
|---|---|---|
| App (CSharp/Layouts) | Code and UI templates | Write C# code, design HTML layouts |
| Database | Data management | Create collections, edit JSON documents |
| Preview | Testing | See your app in action |
Every Fractal Studio project follows this structure:
FractalPlatform.YourApp/ ├── YourAppApplication.cs // Main application class ├── Database/ │ ├── Users/ // Collection name │ │ ├── Document/ │ │ │ ├── 0000000001.json // First document │ │ │ └── 0000000002.json // Second document │ │ ├── UI/ // UI configuration │ │ │ └── Document/ │ │ │ └── 0000000000.json │ │ ├── Enum/ // Dropdown options │ │ │ └── Document/ │ │ │ └── 0000000000.json │ │ └── Event/ // Event handlers │ │ └── Document/ │ │ └── 0000000000.json │ └── GlobalDimensions/ // Shared configurations ├── Layouts/ │ ├── Users.html // Main layout │ └── UsersMobile.html // Mobile layout └── Files/ ├── logo.png // Static files └── styles.css
Your main C# file inherits from DashboardApplication:
public class YourAppApplication : DashboardApplication { // This method runs when app starts public override void OnLogin(FormResult result) { // Initialize your app here Dashboard(); } // This method handles user actions public override bool OnEventDimension(EventInfo info) { // Respond to button clicks, form submissions, etc. return base.OnEventDimension(info); } }
A collection is like a database table, but stored as JSON files. Each collection can have multiple documents (records).
In Fractal Studio, click "Add Collection" and fill in the details:
// Collection: "Products" // First document (0000000001.json): { "Name": "Laptop", "Price": 999.99, "InStock": true, "Categories": ["Electronics", "Computers"] }
Use the built-in methods to query and modify data:
// Get all documents from a collection var products = DocsOf("Products") .ToCollection(); // Query specific documents var laptops = DocsWhere("Products", "{'Name':'Laptop'}") .ToCollection(); // Get a single value var price = DocsWhere("Products", "{'Name':'Laptop'}") .Value("{'Price':$}"); // Returns: "999.99" // Add a new document AddDoc("Products", "{'Name':'Mouse','Price':29.99}"); // Update existing documents ModifyDocsWhere("Products", "{'Name':'Laptop'}") .Update("{'Price':899.99}"); // Delete documents DelDoc("Products", 1); // Delete by ID
Each document has a unique ID stored in its filename:
0000000001.json → ID is 10000000002.json → ID is 2Dimensions are configuration layers that control various aspects of your application without changing code. Think of them as "settings files" for different behaviors.
| Dimension | Purpose | Example Use Case |
|---|---|---|
| UI | Control appearance and behavior | Make fields read-only, set control types |
| Enum | Define dropdown options | Status: Active, Inactive, Pending |
| Event | Map UI actions to C# methods | Button click → SaveData() method |
| Validation | Define data rules | Email must be valid format |
| Sorting | Default sort order | Sort products by price ascending |
| Pagination | Configure paging | Show 10 items per page |
Control how your data is displayed:
{
"Name": {
"ReadOnly": false,
"ControlType": "TextBox",
"MaxLength": 50
},
"Email": {
"ReadOnly": false,
"ControlType": "Email"
},
"Status": {
"ControlType": "ComboBox"
},
"Bio": {
"ControlType": "TextArea",
"Rows": 5
}
}
Define dropdown options:
{
"Status": {
"Items": ["Active", "Inactive", "Pending"]
},
"Country": {
"Items": ["USA", "Canada", "Mexico"]
}
}
You can also configure dimensions programmatically:
// Set UI dimension FirstDocOf("Users") .SetUIDimension("{'Name':{'ReadOnly':true}}") .OpenForm(); // Set Enum dimension var countries = new[] { "USA", "Canada", "Mexico" }; FirstDocOf("Users") .SetDimension(DimensionType.Enum, DQL("{'Country':{'Items':[@Countries]}}", countries)) .OpenForm();
A layout is an HTML template that defines how your data is displayed. Fractal Studio uses special placeholders that get replaced with actual data.
div class="form-container" User Profile div class="field" Name: @Name /div div class="field" Email: @Email /div div class="actions" @Save @Cancel /div /div
| Placeholder | Purpose | Example |
|---|---|---|
@FieldName
|
Replaced with input control | @Name
→ text input |
@BaseUrl
|
Your app's URL | Used for links |
@BaseFilesUrl
|
Files directory URL | For images, CSS, etc. |
Fractal Studio can automatically generate modern, responsive layouts:
src="@BaseFilesUrl/logo.png" alt="Logo">
rel="stylesheet" href="@BaseFilesUrl/styles.css">
href="@BaseUrl/Products">View Products
Create separate layouts for mobile devices by naming them with "Mobile" suffix:
Users.html - Desktop layoutUsersMobile.html - Mobile layoutYour main application class extends DashboardApplication and overrides key methods:
using FractalPlatform.Client.App; using FractalPlatform.Database.Engine; using System.Collections.Generic; namespace FractalPlatform.YourApp { public class YourAppApplication : DashboardApplication { // Called when user logs in public override void OnLogin(FormResult result) { // Initialize your application ShowMainDashboard(); } // Handle user interactions public override bool OnEventDimension(EventInfo info) { switch (info.AttrPath.ToString()) { case "SaveButton": SaveData(info.Collection); return true; case "DeleteButton": DeleteData(info.Collection); return true; } return base.OnEventDimension(info); } } }
// Open a form with first document FirstDocOf("Users") .OpenForm(result => { if (result.Result) { // User clicked Save/OK var name = result.FindFirstValue("Name"); MessageBox($"Hello, {name}!"); } }); // Open form with custom object new { Name = "", Email = "", Age = 0 } .ToCollection("New User") .OpenForm(result => { if (result.Result) { // Save to database AddDoc("Users", result.Collection.ToJson()); } });
// Get all documents var allUsers = DocsOf("Users") .ToCollection(); // Filter by condition var activeUsers = DocsWhere("Users", "{'Status':'Active'}") .ToCollection(); // Get specific document by ID var user = DocsWhere("Users", 1) .ToCollection(); // Count documents var count = DocsWhere("Users", "{'Status':'Active'}") .Count(); // Get single value var email = DocsWhere("Users", "{'Name':'John'}") .Value("{'Email':$}");
// Update documents ModifyDocsWhere("Users", "{'Name':'John'}") .Update("{'Status':'Inactive'}"); // Delete documents ModifyDocsWhere("Users", "{'Status':'Inactive'}") .Delete(); // Add to array ModifyDocsWhere("Users", "{'Name':'John'}") .Update("{'Roles':[Add,'Admin']}");
DQL allows you to pass parameters safely:
// Basic DQL with parameters var userName = "John"; var user = DocsWhere("Users", DQL("{'Name':@Name}", userName)) .ToCollection(); // Multiple parameters var minAge = 18; var status = "Active"; var users = DocsWhere("Users", DQL("{'Age':{'>':@MinAge},'Status':@Status}", minAge, status)) .ToCollection(); // Arrays in DQL var roles = new[] { "Admin", "Manager" }; var dimension = DQL("{'Role':{'Items':[@Roles]}}", roles);
// Show message box MessageBox("Operation completed!"); // Confirmation dialog MessageBox("Are you sure?", "Confirm", MessageBoxButtonType.YesNo, result => { if (result.Result) { // User clicked Yes DeleteRecord(); } }); // Input box InputBox("Enter your name", "User Input", result => { var name = result.FindFirstValue("Enter your name"); MessageBox($"Hello, {name}!"); });
Events connect user actions (button clicks, form submissions) to your C# code. There are several ways to handle events in Fractal Studio.
The primary method for handling button clicks and other actions:
public override bool OnEventDimension(EventInfo info) { // info.AttrPath contains the path to the clicked element var path = info.AttrPath.ToString(); switch (path) { case "SaveButton": // Get data from the form var name = info.Collection.FindFirstValue("Name"); var email = info.Collection.FindFirstValue("Email"); // Save to database AddDoc("Users", DQL("{'Name':@Name,'Email':@Email}", name, email)); MessageBox("User saved successfully!"); return true; case "DeleteButton": MessageBox("Are you sure?", "Confirm Delete", MessageBoxButtonType.YesNo, result => { if (result.Result) { var id = info.Collection.FindFirstIntValue("ID"); DelDoc("Users", id); MessageBox("User deleted!"); } }); return true; case "RefreshButton": ShowUserList(); return true; } return base.OnEventDimension(info); }
Handle form lifecycle events:
public override void OnOpenForm(FormResult result) { // Called when a form is about to open // Good for initializing data if (result.Collection.Name == "Users") { // Pre-fill some data result.Collection.SetFirstValue("CreatedDate", DateTime.Now.ToString()); } base.OnOpenForm(result); } public override void OnCloseForm(FormResult result) { // Called when form closes // Good for cleanup or validation if (result.Result && result.Collection.Name == "Users") { // User clicked Save/OK var email = result.FindFirstValue("Email"); if (!email.Contains("@")) { MessageBox("Invalid email address!"); return; } } base.OnCloseForm(result); }
Define events in JSON configuration:
// In Event dimension JSON: { "SaveButton": { "OnClick": "SaveUser" }, "LoadButton": { "OnClick": "LoadUsers" } } // In your C# code: public void SaveUser(EventInfo info) { var name = info.Collection.FindFirstValue("Name"); AddDoc("Users", DQL("{'Name':@Name}", name)); MessageBox("User saved!"); } public void LoadUsers(EventInfo info) { var users = DocsOf("Users") .ToCollection(); users.OpenForm(); }
// Access form data after submission FirstDocOf("Users") .OpenForm(result => { if (result.Result) // User clicked Save/OK { // Get single value var name = result.FindFirstValue("Name"); // Get typed value var age = result.FindFirstIntValue("Age"); var isActive = result.FindFirstBoolValue("IsActive"); // Get entire collection as JSON var json = result.Collection.ToJson(); // Loop through values result.Collection.ScanKeysAndValues((attr, value) => { Console.WriteLine($"{attr}: {value}"); return true; // Continue scanning }); } else // User clicked Cancel { MessageBox("Operation cancelled"); } });
// Display a list with actions DocsOf("Users") .SetDimension(DimensionType.Patch) .OpenForm(result => { if (result.Result) { // Get patch dimension to see changes var dimension = (PatchDimension)result.Collection .GetDimension(DimensionType.Patch); var patch = dimension.GetPatch(); if (patch.HasChanges) { // Process deleted items patch.DeleteStorage.ToCollection() .ScanKeysAndValues((attr, value) => { if (attr.LastPath == "ID") { DelDoc("Users", value.IntValue); } return true; }); // Process updated items patch.UpdateStorage.ToCollection() .ScanKeysAndValues((attr, value) => { // Handle updates return true; }); } } });
true after handling an eventresult.Result to check if user confirmed actionOnCloseForm before savingDeployment in Fractal Studio involves compiling your code, uploading files, and creating the database on the server.
The easiest way to deploy is using the Deploy button in the dashboard:
// What happens when you click "Deploy": 1. Compile C# Code - Compiles YourAppApplication.cs - Checks for syntax errors - Creates assembly DLL 2. Upload Database - Uploads all JSON files - Creates collections - Optionally recreates database 3. Upload Layouts - Uploads HTML templates - Uploads CSS files - Uploads JavaScript files 4. Upload Files - Uploads static resources - Images, fonts, etc. 5. Register Application - Makes app accessible at URL - Sets up routing
Configure deployment options in Settings:
| Setting | Description | When to Use |
|---|---|---|
| IsRecreateDatabase | Delete and recreate database | When structure changes significantly |
| IsDeployApplication | Upload compiled code | When C# code changes |
| IsDeployLayouts | Upload HTML templates | When layouts change |
| IsDeployDatabase | Upload JSON data | When data structure changes |
| IsDeployFiles | Upload static files | When images/CSS change |
The deployment key controls access to your application:
// Default deployment key var deploymentKey = "sandbox"; // Your app URL structure: https://fraplat.tech/mars/{YourAppName} // With URL tag (custom routing): https://fraplat.tech/mars/{YourAppName}/?tag={YourTag}
You can transfer your application to different environments:
// Transfer application settings: { "BaseUrl": "https://your-server.com", "DeploymentKey": "production", "IsDeployApplication": true, "IsDeployDatabase": true, "IsRecreateDatabase": false, // Keep existing data "IsDeployLayouts": true, "IsDeployFiles": true }
Download your entire project as a ZIP file:
Synchronize your local project with server:
// Pull database from server PullFromSandbox() // From main server PullFromRemote() // From custom server // Use cases: - Team member made changes on server - Testing with production data - Recovering lost local changes - Syncing after manual edits
| Feature | Preview | Deploy |
|---|---|---|
| Speed | Instant | 10-30 seconds |
| Compilation | No | Yes |
| Layout Only | Yes | Everything |
| When to Use | Quick UI changes | Code or data changes |
// Collections: Use singular nouns ✅ User, Product, Order ❌ Users, Products, Orders // Layouts: Match collection names ✅ User.html, UserMobile.html ❌ user_form.html, mobile-view.html // C# Methods: Use PascalCase verbs ✅ SaveUser(), LoadProducts(), DeleteOrder() ❌ save_user(), loadproducts(), delete_order() // Properties: Use PascalCase nouns ✅ UserName, EmailAddress, OrderTotal ❌ userName, email_address, ordertotal
// ✅ GOOD: Flat and simple { "Name": "John Doe", "Email": "john@example.com", "Age": 30, "Status": "Active" } // ✅ GOOD: Nested when logical { "Name": "John Doe", "Address": { "Street": "123 Main St", "City": "New York", "Zip": "10001" } } // ❌ BAD: Too deeply nested { "User": { "Personal": { "Name": { "First": "John", "Last": "Doe" } } } }
// ✅ GOOD: Arrays for lists { "Name": "John Doe", "Roles": ["Admin", "User"], "Orders": [ { "ID": 1, "Total": 99.99, "Date": "2024-01-15" } ] } // ✅ GOOD: Add/Remove from arrays ModifyDocsWhere("Users", "{'Name':'John'}") .Update("{'Roles':[Add,'Manager']}"); ModifyDocsWhere("Users", "{'Name':'John'}") .Update("{'Roles':[Remove,'User']}");
// ✅ GOOD: Always validate user input var email = result.FindFirstValue("Email"); if (string.IsNullOrEmpty(email) || !email.Contains("@")) { MessageBox("Please enter a valid email address"); return; } // ✅ GOOD: Check if data exists var count = DocsWhere("Users", "{'Email':@Email}", email) .Count(); if (count > 0) { MessageBox("Email already exists!"); return; } // ✅ GOOD: Confirm destructive actions MessageBox("Are you sure you want to delete this user?", "Confirm Delete", MessageBoxButtonType.YesNo, confirmResult => { if (confirmResult.Result) { DelDoc("Users", userId); } });
// ✅ GOOD: Create helper methods private void ShowUserForm(int? userId = null) { Collection user; if (userId.HasValue) { // Edit existing user user = DocsWhere("Users", userId.Value) .ToCollection(); } else { // New user user = new { Name = "", Email = "" } .ToCollection("New User"); } user.SetUIDimension("{'Style':'Save:Save'}") .OpenForm(result => { if (result.Result) { SaveUser(result.Collection, userId); } }); } private void SaveUser(Collection data, int? userId) { if (userId.HasValue) { ModifyDocsWhere("Users", userId.Value) .Update(data.ToJson()); } else { AddDoc("Users", data.ToJson()); } MessageBox("User saved successfully!"); ShowUserList(); }
// ❌ BAD: Loading all data unnecessarily var allUsers = DocsOf("Users").ToCollection(); var activeUsers = new List<User>(); foreach (var user in allUsers) { if (user.Status == "Active") activeUsers.Add(user); } // ✅ GOOD: Filter in query var activeUsers = DocsWhere("Users", "{'Status':'Active'}") .ToCollection(); // ✅ GOOD: Use pagination for large datasets DocsOf("Users") .SetDimension(DimensionType.Pagination, "{'Page':{'Size':20}}") .OpenForm();
// ❌ BAD: Reopening form after every action public void AddItem() { AddDoc("Items", "{'Name':'New Item'}"); ShowItemList(); // Reopens entire form } // ✅ GOOD: Use Patch dimension for dynamic updates DocsOf("Items") .SetDimension(DimensionType.Patch) .OpenForm(result => { var dimension = (PatchDimension)result.Collection .GetDimension(DimensionType.Patch); var patch = dimension.GetPatch(); // Process changes without reopening ProcessPatch(patch); });
// ✅ GOOD: Confirm actions AddDoc("Users", userData); MessageBox("User created successfully!"); // ✅ GOOD: Show errors clearly if (!IsValidEmail(email)) { MessageBox("Invalid email format. Please use: name@domain.com", "Validation Error", MessageBoxButtonType.Ok); return; } // ✅ GOOD: Confirm before deletion MessageBox($"Delete user '{userName}'? This cannot be undone.", "Confirm Delete", MessageBoxButtonType.YesNo, result => { /* handle */ });
// ✅ GOOD: Always validate on server side public override void OnCloseForm(FormResult result) { if (result.Result && result.Collection.Name == "Users") { var email = result.FindFirstValue("Email"); var age = result.FindFirstIntValue("Age"); // Validate email format if (!IsValidEmail(email)) { MessageBox("Invalid email format"); return; } // Validate age range if (age < 18 || age > 120) { MessageBox("Age must be between 18 and 120"); return; } } }
// ❌ BAD: String concatenation (potential injection) var userName = userInput; var query = "{'Name':'" + userName + "'}"; var user = DocsWhere("Users", query); // ✅ GOOD: Use DQL with parameters var userName = userInput; var user = DocsWhere("Users", DQL("{'Name':@UserName}", userName));
| Problem | Cause | Solution |
|---|---|---|
| Form doesn't open | Collection name misspelled | Check exact spelling in Database tab |
| Data not saving | Not calling commit/update | Use AddDoc or ModifyDocsWhere |
| Layout looks wrong | CSS not loading | Check @ BaseFilesUrl path |
| Button does nothing | Event not handled | Add case in OnEventDimension |
| Compilation error | Syntax error in C# | Check Deploy logs for details |
// Show values for debugging MessageBox($"Debug: Value = {someValue}"); // Log collection contents var json = myCollection.ToJson(); MessageBox($"Collection: {json}"); // Check if data exists var count = DocsOf("Users").Count(); MessageBox($"Total users: {count}"); // Verify paths in events public override bool OnEventDimension(EventInfo info) { MessageBox($"Event path: {info.AttrPath}"); return base.OnEventDimension(info); }
Official Wiki - Complete reference for all dimensions and features
Telegram Group - Get help from developers and the creator
Study template projects in Fractal Studio - HelloWorld, TodoList, etc.
Learn database operations and data consistency from the transactions documentation