Working with Controllers
This section provides an in-depth guide to developing custom controllers in Magentrix. You'll learn how to create action methods, work with parameters, access user information, manage cookies, handle validation, and leverage the powerful utilities provided by the AspxController base class.
Table of Contents
- Action Methods
- HTTP Method Handling (GET vs POST)
- Working with Parameters
- Accessing User Information
- Working with Cookies
- Displaying Messages to Users
- Model Binding and Validation
- Using DataBag for Temporary Storage
- Database Operations
- Error Handling
- Summary
- Next Steps
- Quick Reference
Action Methods
Action methods are public methods in your controller that respond to HTTP requests. Each action executes business logic and returns an ActionResponse.
Basic Action Structure
public ActionResponse MyAction()
{
// Your business logic here
return View();
}
Action Method Rules
✅ All public methods are actions - Any public method in your controller is automatically treated as an action
❌ No method overloads - You cannot create multiple actions with the same name unless they handle different HTTP verbs
✅ The Index() action is special:
- For Page Controllers (
/aspx/): It's the default action and cannot accept parameters - Must override the base class implementation
- Accessed when no action is specified in the URL
❌ Non-action methods - Make methods private if they shouldn't be accessible via URL:
public class MyController : AspxController
{
// This IS an action - accessible via URL
public ActionResponse PublicAction()
{
return View();
}
// This is NOT an action - helper method only
private string HelperMethod()
{
return "Some value";
}
}
The Index Action
For Page Controllers:
public class ContactManagerController : AspxController
{
// URL: /aspx/ContactManager or /aspx/ContactManager/Index
public override ActionResponse Index()
{
var contacts = Database.Query<Contact>().Limit(20).ToList();
return View(contacts);
}
}
⚠ Important: The Index() action cannot have parameters for Page Controllers.
For Standalone Controllers:
public class ContactApiController : AspxController
{
// URL: /acls/ContactApi/Index (must be explicitly specified)
public ActionResponse Index()
{
var contacts = Database.Query<Contact>().Limit(20).ToList();
return Json(contacts, JsonRequestBehavior.AllowGet);
}
}
Custom Action Methods
public class ContactManagerController : AspxController
{
// URL: /aspx/ContactManager/Search?name=John
public ActionResponse Search(string name)
{
var results = Database.Query<Contact>()
.Where(c => c.FirstName.Contains(name) || c.LastName.Contains(name))
.ToList();
return View(results);
}
// URL: /aspx/ContactManager/Details?id=003R000000haXk3IAE
public ActionResponse Details(string id)
{
var contact = Database.Retrieve<Contact>(id);
return View(contact);
}
// URL: /aspx/ContactManager/Export
public ActionResponse Export()
{
var contacts = Database.Query<Contact>().ToList();
// Generate CSV
var csv = new StringBuilder();
csv.AppendLine("First Name,Last Name,Email");
foreach (var contact in contacts)
csv.AppendLine($"{contact.FirstName},{contact.LastName},{contact.Email}");
var bytes = Encoding.UTF8.GetBytes(csv.ToString());
return File(bytes, "text/csv", "contacts.csv");
}
}
HTTP Method Handling (GET vs POST)
By default, action methods respond to GET requests. Use attributes to restrict actions to specific HTTP verbs.
GET Actions (Default)
public class ContactManagerController : AspxController
{
// This responds to GET requests by default
public override ActionResponse Index()
{
return View(new Contact());
}
// This also responds to GET requests
public ActionResponse Edit(string id)
{
var contact = Database.Retrieve<Contact>(id);
return View(contact);
}
}
POST Actions
Use the [HttpPost] attribute to restrict an action to POST requests only:
public class ContactManagerController : AspxController
{
// GET: Display empty form
public override ActionResponse Index()
{
return View(new Contact());
}
// POST: Process form submission
[HttpPost]
public ActionResponse Index(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Contact created successfully!");
return RedirectToAction("Index");
}
return View(model);
}
}
GET and POST Pattern
The most common pattern is to have two actions with the same name - one for GET (display form) and one for POST (process form):
public class OpportunityController : AspxController
{
// GET: /aspx/Opportunity/Create
// Shows the create form
public ActionResponse Create()
{
var model = new Opportunity
{
Stage = "Prospecting",
CloseDate = DateTime.Now.AddDays(30)
};
return View(model);
}
// POST: /aspx/Opportunity/Create
// Processes the submitted form
[HttpPost]
public ActionResponse Create(Opportunity model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Opportunity created successfully!");
return RedirectToAction("Index");
}
// If validation fails, redisplay the form with errors
return View(model);
}
}
AcceptVerbs Attribute
To allow an action to respond to multiple HTTP verbs:
[AcceptVerbs("GET", "POST")]
public ActionResponse FlexibleAction(string data)
{
if (Request.HttpMethod == "POST")
{
// Handle POST logic
return Json(new { success = true });
}
else
{
// Handle GET logic
return View();
}
}
⚠ Caution: If you mark an action to respond to multiple HTTP verbs, you cannot create overloads for the same action name.
Working with Parameters
Actions can accept parameters from the URL query string or the request body (for POST requests).
Query String Parameters
Parameters are automatically mapped from the URL to action method parameters:
// URL: /aspx/ContactManager/Search?firstName=John&lastName=Doe
public ActionResponse Search(string firstName, string lastName)
{
var results = Database.Query<Contact>()
.Where(c => c.FirstName == firstName && c.LastName == lastName)
.ToList();
return View(results);
}
Reading Parameters with AspxPage
You can also read parameters manually using AspxPage.GetParameter():
public ActionResponse MyAction()
{
var id = AspxPage.GetParameter("id");
var filter = AspxPage.GetParameter("filter");
if (string.IsNullOrEmpty(id))
return RedirectToError("ID parameter is required.");
var contact = Database.Retrieve<Contact>(id);
return View(contact);
}
Multiple Parameter Types
// URL: /aspx/Reports/Generate?accountId=001R0000&startDate=2025-01-01&includeDetails=true
public ActionResponse Generate(string accountId, DateTime startDate, bool includeDetails)
{
var account = Database.Retrieve<Account>(accountId);
var opportunities = Database.Query<Opportunity>()
.Where(o => o.AccountId == accountId && o.CloseDate >= startDate)
.ToList();
if (includeDetails)
{
// Include detailed information
}
return Pdf(opportunities, $"Report_{account.Name}.pdf");
}
Magentrix automatically converts query string values to the appropriate types (string, int, bool, DateTime, etc.).
POST Body Parameters (Model Binding)
For POST requests, parameters are automatically bound from the form data:
[HttpPost]
public ActionResponse CreateContact(Contact model)
{
// model is automatically populated from POST form data
if (ModelState.IsValid)
{
Database.Insert(model);
return Json(new { success = true, id = model.Id });
}
return Json(new { success = false, errors = ModelState });
}
Complex Parameter Examples
Example 1: Search with Multiple Filters
public ActionResponse AdvancedSearch(
string name,
string type,
string industry,
int? minRevenue,
int? maxRevenue)
{
var query = Database.Query<Account>();
if (!string.IsNullOrEmpty(name))
query = query.Where(a => a.Name.Contains(name));
if (!string.IsNullOrEmpty(type))
query = query.Where(a => a.Type == type);
if (!string.IsNullOrEmpty(industry))
query = query.Where(a => a.Industry == industry);
if (minRevenue.HasValue)
query = query.Where(a => a.AnnualRevenue >= minRevenue.Value);
if (maxRevenue.HasValue)
query = query.Where(a => a.AnnualRevenue <= maxRevenue.Value);
var results = query.ToList();
return View(results);
}
Example 2: Pagination Parameters
// URL: /aspx/ContactList/Index?page=2&pageSize=50
public ActionResponse Index(int page = 1, int pageSize = 25)
{
int skip = (page - 1) * pageSize;
var contacts = Database.Query<Contact>()
.OrderBy(c => c.LastName)
.Skip(skip)
.Take(pageSize)
.ToList();
int totalCount = Database.Query<Contact>().Count();
int totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
var viewModel = new PaginatedViewModel
{
Items = contacts,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages
};
return View(viewModel);
}
Controllers provide comprehensive access to information about the currently logged-in user through the UserInfo and SystemInfo properties.
UserInfo Properties
The UserInfo object provides information about the current user:
public ActionResponse MyProfile()
{
// Access current user's email
var email = UserInfo.Email;
// Access current user's full name
var name = UserInfo.Name;
// Check if user is a portal user (Partner/Customer)
var isPortalUser = UserInfo.IsPortalUser;
// Check if user is an employee
var isEmployee = !UserInfo.IsPortalUser;
return View();
}
Accessing Portal User's Contact and Account
For portal users (Partner/Customer users), you can access their associated Contact and Account records:
public ActionResponse Dashboard()
{
if (UserInfo.IsPortalUser)
{
// Access the user's Contact record
var userContact = UserInfo.Contact;
// Access the user's Account record
var userAccount = UserInfo.Account;
// Now you can access any field on these records
var phone = userContact?.Phone;
var accountName = userAccount?.Name;
var mailingStreet = userContact?.MailingStreet;
var accountType = userAccount?.Type;
var viewModel = new DashboardViewModel
{
UserName = userContact?.Name,
CompanyName = userAccount?.Name,
Phone = phone
};
return View(viewModel);
}
else
{
// Employee user logic
return View();
}
}
SystemInfo Properties
The SystemInfo object provides portal-level information:
public ActionResponse SystemStatus()
{
// Check if current user is a guest (not logged in)
var isGuest = SystemInfo.IsGuestUser;
// Get portal URL
var portalUrl = SystemInfo.PortalUrl;
// Other system information
// [TODO: clarify - what other properties are available on SystemInfo?]
return View();
}
Practical Examples
Example 1: Showing User-Specific Data
public override ActionResponse Index()
{
if (UserInfo.IsPortalUser)
{
// Show only opportunities related to the user's account
var opportunities = Database.Query<Opportunity>()
.Where(o => o.AccountId == UserInfo.Account.Id)
.OrderByDescending(o => o.CreatedDate)
.ToList();
return View(opportunities);
}
else
{
// Employee users see all opportunities
var opportunities = Database.Query<Opportunity>()
.OrderByDescending(o => o.CreatedDate)
.ToList();
return View(opportunities);
}
}
Example 2: User-Specific Welcome Message
public override ActionResponse Index()
{
string welcomeMessage;
if (UserInfo.IsPortalUser)
welcomeMessage = $"Welcome {UserInfo.Contact.FirstName}! Your account: {UserInfo.Account.Name}";
else
welcomeMessage = $"Welcome {UserInfo.Name}";
DataBag.WelcomeMessage = welcomeMessage;
return View();
}
Example 3: Restricting Access Based on User Type
public ActionResponse AdminDashboard()
{
// Only allow employee users
if (UserInfo.IsPortalUser)
return UnauthorizedAccessResponse();
// Employee-only logic
return View();
}
Example 4: Logging User Activity
[HttpPost]
public ActionResponse UpdateOpportunity(Opportunity model)
{
if (ModelState.IsValid)
{
// Track who made the update
model.LastModifiedBy = UserInfo.Name;
model.LastModifiedDate = DateTime.Now;
Database.Update(model);
AspxPage.AddMessage("Opportunity updated successfully.");
return RedirectToAction("Details", new { id = model.Id });
}
return View(model);
}
Working with Cookies
Controllers provide utilities for reading, writing, and removing browser cookies through the AspxPage object.
Reading Cookies
public ActionResponse MyAction()
{
// Read a cookie
var cookie = AspxPage.GetCookie("myCookieName");
if (cookie != null)
{
var value = cookie.Value;
// Use the cookie value
}
else
{
// Cookie doesn't exist
}
return View();
}
Writing Cookies
public ActionResponse SetPreferences(string theme, string language)
{
// Create a new cookie
var themeCookie = new HttpCookie("userTheme");
themeCookie.Value = theme;
themeCookie.Expires = DateTime.Now.AddDays(30); // Cookie expires in 30 days
// Save the cookie
AspxPage.SetCookie(themeCookie);
// Create another cookie
var langCookie = new HttpCookie("userLanguage");
langCookie.Value = language;
langCookie.Expires = DateTime.Now.AddYears(1); // Cookie expires in 1 year
AspxPage.SetCookie(langCookie);
AspxPage.AddMessage("Preferences saved!");
return RedirectToAction("Index");
}
Updating Cookies
public ActionResponse UpdateCookie()
{
// Read existing cookie
var cookie = AspxPage.GetCookie("visitCount");
if (cookie != null)
{
// Update the value
int count = int.Parse(cookie.Value);
count++;
cookie.Value = count.ToString();
}
else
{
// Create new cookie if it doesn't exist
cookie = new HttpCookie("visitCount");
cookie.Value = "1";
cookie.Expires = DateTime.Now.AddYears(1);
}
// Save the updated cookie
AspxPage.SetCookie(cookie);
return View();
}
Removing Cookies
public ActionResponse Logout()
{
// Remove a cookie
AspxPage.RemoveCookie("userTheme");
AspxPage.RemoveCookie("userLanguage");
AspxPage.RemoveCookie("visitCount");
return Redirect("~/login");
}
Practical Cookie Examples
Example 1: Remember User Preferences
public override ActionResponse Index()
{
// Check if user has a theme preference
var themeCookie = AspxPage.GetCookie("userTheme");
if (themeCookie != null)
DataBag.Theme = themeCookie.Value;
else
DataBag.Theme = "default";
return View();
}
Example 2: Track Last Visit
public override ActionResponse Index()
{
HttpCookie lastVisitCookie = AspxPage.GetCookie("lastVisit");
if (lastVisitCookie != null)
{
var lastVisit = DateTime.Parse(lastVisitCookie.Value);
var timeSinceVisit = DateTime.Now - lastVisit;
DataBag.WelcomeMessage = $"Welcome back! Your last visit was {timeSinceVisit.Days} days ago.";
}
else
DataBag.WelcomeMessage = "Welcome! This is your first visit.";
// Update last visit cookie
var newVisitCookie = new HttpCookie("lastVisit");
newVisitCookie.Value = DateTime.Now.ToString();
newVisitCookie.Expires = DateTime.Now.AddYears(1);
AspxPage.SetCookie(newVisitCookie);
return View();
}
Example 3: Shopping Cart Cookie
public ActionResponse AddToCart(string productId)
{
var cartCookie = AspxPage.GetCookie("shoppingCart");
List<string> cartItems;
if (cartCookie != null)
{
// Deserialize existing cart
cartItems = JsonConvert.DeserializeObject<List<string>>(cartCookie.Value);
}
else
cartItems = new List<string>();
// Add product to cart
cartItems.Add(productId);
// Save updated cart
var updatedCookie = new HttpCookie("shoppingCart");
updatedCookie.Value = JsonConvert.SerializeObject(cartItems);
updatedCookie.Expires = DateTime.Now.AddDays(7);
AspxPage.SetCookie(updatedCookie);
return Json(new { success = true, itemCount = cartItems.Count });
}
Displaying Messages to Users
Controllers can display formatted messages on Active Pages using AspxPage methods. These messages appear in a consistent, user-friendly format at the top of the page.
Message Types
Magentrix supports three types of messages:
- Error Messages (red) - For validation errors and failures
- Warning Messages (orange) - For cautionary information
- Informational Messages (blue) - For success confirmations and general info
Displaying Error Messages
public ActionResponse Save(Contact model)
{
if (string.IsNullOrEmpty(model.Email))
{
// Show an error message at the top of the page
AspxPage.AddError("Email address is required.");
return View(model);
}
Database.Insert(model);
return RedirectToAction("Index");
}
Displaying Field-Specific Errors
You can associate error messages with specific fields:
[HttpPost]
public ActionResponse Create(Contact model)
{
// Validate email format
if (!model.Email.Contains("@"))
{
// Show error next to the Email field
AspxPage.AddError("Invalid email format.", "Email");
return View(model);
}
// Validate phone number
if (string.IsNullOrEmpty(model.Phone))
{
AspxPage.AddError("Phone number is required.", "Phone");
return View(model);
}
Database.Insert(model);
return RedirectToAction("Index");
}
The second parameter ("Email", "Phone") specifies the field key to associate the error with.
Displaying Success Messages
[HttpPost]
public ActionResponse Create(Contact model)
{
if (ModelState.IsValid)
{
Database.Insert(model);
// Show a success message (blue background)
AspxPage.AddMessage("Contact created successfully!");
return RedirectToAction("Index");
}
return View(model);
}
Displaying Warning Messages
public ActionResponse Delete(string id)
{
var contact = Database.Retrieve<Contact>(id);
// Check if contact has related records
var opportunities = Database.Query<Opportunity>()
.Where(o => o.ContactId == id)
.Count();
if (opportunities > 0)
{
// Show a warning message (orange background)
AspxPage.AddWarning($"This contact has {opportunities} related opportunities. Deleting may affect data integrity.");
}
return View(contact);
}
Multiple Messages
You can display multiple messages of different types:
public ActionResponse ProcessBatch()
{
int successCount = 0;
int errorCount = 0;
var contacts = Database.Query<Contact>().ToList();
var contactsToUpdate = new List<Contact>();
var failedContacts = new List<string>();
foreach (var contact in contacts)
{
try
{
// Process contact (business logic only, no database calls)
contact.LastProcessedDate = DateTime.Now;
contact.Status = "Processed";
contactsToUpdate.Add(contact);
successCount++;
}
catch (Exception ex)
{
SystemInfo.Debug($"Error processing contact {contact.Id}: {ex.Message}");
failedContacts.Add(contact.Id);
errorCount++;
}
}
// Single batch update operation
if (contactsToUpdate.Count > 0)
{
Database.Update(contactsToUpdate);
}
if (successCount > 0)
{
AspxPage.AddMessage($"{successCount} contacts processed successfully.");
}
if (errorCount > 0)
{
AspxPage.AddError($"{errorCount} contacts failed to process.");
}
return RedirectToAction("Index");
}
Adding Messages in Active Pages
For messages to display, your Active Page must include the <aspx:ViewMessages> tag:
<aspx:AspxPage runat="server" title="Contact Manager">
<body>
<!-- This tag displays messages from the controller -->
<aspx:ViewMessages runat="server"/>
<aspx:ViewPanel runat='server' title='Contact Form'>
<!-- Page content here -->
</aspx:ViewPanel>
</body>
</aspx:AspxPage>
Practical Message Examples
Example 1: Form Validation with Multiple Errors
[HttpPost]
public ActionResponse CreateOpportunity(Opportunity model)
{
bool hasErrors = false;
if (string.IsNullOrEmpty(model.Name))
{
AspxPage.AddError("Opportunity Name is required.", "Name");
hasErrors = true;
}
if (model.Amount <= 0)
{
AspxPage.AddError("Amount must be greater than zero.", "Amount");
hasErrors = true;
}
if (model.CloseDate < DateTime.Now)
{
AspxPage.AddError("Close Date cannot be in the past.", "CloseDate");
hasErrors = true;
}
if (hasErrors)
return View(model);
Database.Insert(model);
AspxPage.AddMessage("Opportunity created successfully!");
return RedirectToAction("Index");
}
Example 2: Import Results with Mixed Outcomes
public ActionResponse ImportContacts(List<Contact> contacts)
{
int imported = 0;
int skipped = 0;
int errors = 0;
foreach (var contact in contacts)
{
try
{
// Check for duplicates
var existing = Database.Query<Contact>()
.Where(c => c.Email == contact.Email)
.FirstOrDefault();
if (existing != null)
{
skipped++;
continue;
}
Database.Insert(contact);
imported++;
}
catch (Exception)
{
errors++;
}
}
if (imported > 0)
{
AspxPage.AddMessage($"{imported} contacts imported successfully.");
}
if (skipped > 0)
{
AspxPage.AddWarning($"{skipped} contacts were skipped (duplicates).");
}
if (errors > 0)
{
AspxPage.AddError($"{errors} contacts failed to import due to errors.");
}
return RedirectToAction("Index");
}
Model Binding and Validation
Magentrix automatically binds form data to C# objects and validates them using ModelState.
Automatic Model Binding
When a form is submitted (POST request), Magentrix automatically populates your model object with the form data:
[HttpPost]
public ActionResponse Create(Contact model)
{
// 'model' is automatically populated with form data
// model.FirstName, model.LastName, model.Email, etc.
if (ModelState.IsValid)
{
Database.Insert(model);
return RedirectToAction("Index");
}
return View(model);
}
Checking ModelState
ModelState.IsValid returns true if all validation rules pass:
[HttpPost]
public ActionResponse Save(Opportunity model)
{
if (ModelState.IsValid)
{
// All validation passed
Database.Update(model);
AspxPage.AddMessage("Opportunity saved successfully.");
return RedirectToAction("Details", new { id = model.Id });
}
else
{
// Validation failed - redisplay form with errors
AspxPage.AddError("Please correct the errors below.");
return View(model);
}
}
Accessing Validation Errors
You can inspect individual validation errors:
[HttpPost]
public ActionResponse Create(Contact model)
{
if (!ModelState.IsValid)
{
// Log all validation errors
foreach (var key in ModelState.Keys)
{
var errors = ModelState[key].Errors;
foreach (var error in errors)
{
SystemInfo.Debug($"Field: {key}, Error: {error.ErrorMessage}");
}
}
AspxPage.AddError("Please correct the validation errors.");
return View(model);
}
Database.Insert(model);
return RedirectToAction("Index");
}
Accessing Old and New Values
When using the [SerializeViewData] attribute, you can access both the original value and the modified value:
[SerializeViewData]
public ActionResponse Edit(string id)
{
var opportunity = Database.Retrieve<Opportunity>(id);
DataBag.OriginalStage = opportunity.Stage;
return View(opportunity);
}
[HttpPost]
public ActionResponse Edit(Opportunity model)
{
if (ModelState.IsValid)
{
// Access original value
var originalStage = DataBag.OriginalStage;
// Access new value
var newStage = model.Stage;
// Check if stage changed
if (originalStage != newStage)
{
AspxPage.AddMessage($"Stage changed from {originalStage} to {newStage}.");
}
Database.Update(model);
return RedirectToAction("Index");
}
return View(model);
}
You can also use ModelState to access old and new values:
[HttpPost]
public ActionResponse Edit(Opportunity model)
{
if (ModelState.IsValid)
{
// Access new value (user input)
var newValue = ModelState["Amount"].Value.AttemptedValue;
// Access old value (original from GET)
var oldValue = ModelState["Amount"].Value.RawValue;
Database.Update(model);
return RedirectToAction("Index");
}
return View(model);
}
Manual Validation
You can add custom validation errors to ModelState:
[HttpPost]
public ActionResponse Create(Opportunity model)
{
// Custom business rule validation
if (model.Stage == "Closed Won" && model.Amount <= 0)
{
ModelState.AddModelError("Amount", "Amount must be greater than zero for Closed Won opportunities.");
}
if (model.CloseDate < DateTime.Now && model.Stage != "Closed Lost" && model.Stage != "Closed Won")
{
ModelState.AddModelError("CloseDate", "Close Date cannot be in the past for open opportunities.");
}
if (ModelState.IsValid)
{
Database.Insert(model);
return RedirectToAction("Index");
}
return View(model);
}
Practical Validation Examples
Example 1: Complex Business Rule Validation
[HttpPost]
public ActionResponse CreateOpportunity(Opportunity model)
{
// Validate required fields based on stage
if (model.Stage == "Closed Won")
{
if (model.Amount <= 0)
{
ModelState.AddModelError("Amount", "Amount is required for Closed Won opportunities.");
}
if (model.CloseDate == null)
{
ModelState.AddModelError("CloseDate", "Close Date is required for Closed Won opportunities.");
}
if (string.IsNullOrEmpty(model.AccountId))
{
ModelState.AddModelError("AccountId", "Account is required for Closed Won opportunities.");
}
}
if (model.Stage == "Closed Lost")
{
if (string.IsNullOrEmpty(model.LostReason))
{
ModelState.AddModelError("LostReason", "Lost Reason is required for Closed Lost opportunities.");
}
}
// Validate date range
if (model.CloseDate.HasValue && model.CloseDate.Value < DateTime.Now.AddDays(-1))
{
ModelState.AddModelError("CloseDate", "Close Date cannot be more than 1 day in the past.");
}
// Validate discount limit
if (model.Discount > 25)
{
ModelState.AddModelError("Discount", "Discounts over 25% require manager approval.");
}
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Opportunity created successfully!");
return RedirectToAction("Index");
}
AspxPage.AddError("Please correct the validation errors below.");
return View(model);
}
Example 2: Cross-Field Validation
[HttpPost]
public ActionResponse CreateContract(Contract model)
{
// Validate that end date is after start date
if (model.EndDate <= model.StartDate)
{
ModelState.AddModelError("EndDate", "End Date must be after Start Date.");
}
// Validate contract value based on account type
var account = Database.Retrieve(model.AccountId);
if (account.Type == "Small Business" && model.ContractValue > 50000)
{
ModelState.AddModelError("ContractValue", "Contract value exceeds the limit for Small Business accounts.");
}
// Validate renewal contract requires original contract
if (model.Type == "Renewal" && string.IsNullOrEmpty(model.OriginalContractId))
{
ModelState.AddModelError("OriginalContractId", "Original Contract is required for Renewal contracts.");
}
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Contract created successfully!");
return RedirectToAction("Details", new { id = model.Id });
}
return View(model);
}
Example 3: Unique Value Validation
[HttpPost]
public ActionResponse CreateAccount(Account model)
{
// Check for duplicate account name
var existingAccount = Database.Query<Account>()
.Where(a => a.Name == model.Name)
.FirstOrDefault();
if (existingAccount != null)
{
ModelState.AddModelError("Name", "An account with this name already exists.");
}
// Check for duplicate email
if (!string.IsNullOrEmpty(model.Email))
{
var emailExists = Database.Query<Account>()
.Where(a => a.Email == model.Email)
.Any();
if (emailExists)
{
ModelState.AddModelError("Email", "This email address is already registered to another account.");
}
}
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Account created successfully!");
return RedirectToAction("Index");
}
return View(model);
}
Using DataBag for Temporary Storage
DataBag is a dynamic property bag that allows you to pass temporary data from controllers to views or persist data across GET and POST requests when using the [SerializeViewData] attribute.
Basic DataBag Usage
public override ActionResponse Index()
{
// Store values in DataBag
DataBag.PageTitle = "Welcome to Contact Manager";
DataBag.ShowWelcomeMessage = true;
DataBag.ItemCount = 42;
return View();
}
Accessing in Active Page:
<aspx:AspxPage runat='server' title='{!DataBag.PageTitle}'>
<body>
<!-- Use DataBag values in the page -->
<h1>{!DataBag.PageTitle}</h1>
<aspx:Condition runat='server' test='{!DataBag.ShowWelcomeMessage}'>
<p>Welcome! You have {!DataBag.ItemCount} items.</p>
</aspx:Condition>
</body>
</aspx:AspxPage>
Passing Complex Objects
public ActionResponse Dashboard()
{
var stats = new DashboardStats
{
TotalAccounts = Database.Query<Account>().Count(),
TotalContacts = Database.Query<Contact>().Count(),
OpenOpportunities = Database.Query<Opportunity>()
.Where(o => o.IsClosed == false)
.Count()
};
DataBag.Statistics = stats;
DataBag.LastUpdated = DateTime.Now;
return View();
}
Persisting DataBag Across Requests
Use the [SerializeViewData] attribute to persist DataBag values across GET and POST requests:
[SerializeViewData]
public ActionResponse Edit(string id)
{
var contact = Database.Retrieve<Contact>(id);
// Store additional data in DataBag
DataBag.PageTitle = $"Edit Contact: {contact.FirstName} {contact.LastName}";
DataBag.OriginalEmail = contact.Email;
DataBag.EditStartTime = DateTime.Now;
return View(contact);
}
[HttpPost]
public ActionResponse Edit(Contact model)
{
if (ModelState.IsValid)
{
// DataBag values are still available from the GET request
var originalEmail = DataBag.OriginalEmail;
var editStartTime = (DateTime)DataBag.EditStartTime;
// Check if email changed
if (originalEmail != model.Email)
{
AspxPage.AddMessage($"Email address changed from {originalEmail} to {model.Email}.");
}
// Calculate edit duration
TimeSpan duration = DateTime.Now - editStartTime;
SystemInfo.Debug($"Edit took {duration.TotalSeconds} seconds");
Database.Update(model);
return RedirectToAction("Index");
}
return View(model);
}
DataBag vs Model
Use Model when:
- Passing entity data to the view
- Working with database records
- Form submission and model binding
Use DataBag when:
- Passing metadata (page titles, flags, counts)
- Storing temporary UI state
- Passing lookup data (dropdown options, etc.)
- Tracking workflow state across requests
Practical DataBag Examples
Example 1: Multi-Step Wizard
[SerializeViewData]
public ActionResponse Step1()
{
DataBag.WizardStep = 1;
DataBag.TotalSteps = 3;
return View(new OpportunityWizardModel());
}
[HttpPost]
[SerializeViewData]
public ActionResponse Step1(OpportunityWizardModel model)
{
if (ModelState.IsValid)
{
DataBag.Step1Data = model;
DataBag.WizardStep = 2;
return View("Step2", new ProductSelectionModel());
}
DataBag.WizardStep = 1;
return View(model);
}
[HttpPost]
[SerializeViewData]
public ActionResponse Step2(ProductSelectionModel model)
{
if (ModelState.IsValid)
{
DataBag.Step2Data = model;
DataBag.WizardStep = 3;
return View("Step3", new ReviewModel());
}
DataBag.WizardStep = 2;
return View(model);
}
[HttpPost]
public ActionResponse Step3Confirm()
{
// Retrieve data from all steps
var step1Data = (OpportunityWizardModel)DataBag.Step1Data;
var step2Data = (ProductSelectionModel)DataBag.Step2Data;
// Create the opportunity
var opportunity = new Opportunity
{
Name = step1Data.Name,
AccountId = step1Data.AccountId,
Amount = step2Data.TotalAmount,
CloseDate = step1Data.CloseDate
};
Database.Insert(opportunity);
AspxPage.AddMessage("Opportunity created successfully!");
return RedirectToAction("Index");
}
Example 2: Passing Dropdown Options
public ActionResponse Create()
{
// Populate dropdown options
var accountTypes = new List<SelectListItem>
{
new SelectListItem { Text = "Partner", Value = "Partner" },
new SelectListItem { Text = "Customer", Value = "Customer" },
new SelectListItem { Text = "Prospect", Value = "Prospect" }
};
var industries = Database.Query<Account>()
.Select(a => a.Industry)
.Distinct()
.OrderBy(i => i)
.ToList();
DataBag.AccountTypes = accountTypes;
DataBag.Industries = industries;
DataBag.PageTitle = "Create New Account";
return View(new Account());
}
Example 3: Permission Flags
public ActionResponse Details(string id)
{
var opportunity = Database.Retrieve<Opportunity>(id);
// Set permission flags based on user role
DataBag.CanEdit = CheckEditPermission(opportunity);
DataBag.CanDelete = CheckDeletePermission(opportunity);
DataBag.CanApprove = CheckApprovePermission(opportunity);
// Set UI state
DataBag.ShowWarning = opportunity.Amount > 100000;
DataBag.WarningMessage = "High-value opportunity requires manager approval.";
return View(opportunity);
}
private bool CheckEditPermission(Opportunity opp)
{
// Check if user owns the opportunity or is a manager
if (UserInfo.IsPortalUser)
{
return opp.AccountId == UserInfo.Account.Id;
}
return true; // Employees can always edit
}
private bool CheckDeletePermission(Opportunity opp)
{
// Only allow deletion if opportunity is not closed
return !opp.IsClosed;
}
private bool CheckApprovePermission(Opportunity opp)
{
// Only employees can approve
return !UserInfo.IsPortalUser;
}
Database Operations
Controllers have full access to database operations through the Database property.
Basic CRUD Operations
Create (Insert)
public ActionResponse CreateContact()
{
var contact = new Contact
{
FirstName = "John",
LastName = "Doe",
Email = "john.doe@example.com",
Phone = "(555) 123-4567"
};
Database.Insert(contact);
// After insert, the contact.Id is populated
AspxPage.AddMessage($"Contact created with ID: {contact.Id}");
return RedirectToAction("Index");
}
Read (Retrieve)
public ActionResponse Details(string id)
{
// Retrieve a single record by ID
Contact contact = Database.Retrieve<Contact>(id);
if (contact == null)
{
return PageNotFound();
}
return View(contact);
}
Update
[HttpPost]
public ActionResponse Edit(Contact model)
{
if (ModelState.IsValid)
{
Database.Update(model);
AspxPage.AddMessage("Contact updated successfully!");
return RedirectToAction("Details", new { id = model.Id });
}
return View(model);
}
Delete
[HttpPost]
public ActionResponse Delete(string id)
{
Database.Delete<Contact>(id);
AspxPage.AddMessage("Contact deleted successfully.");
return RedirectToAction("Index");
}
Querying Data
Simple Query
public ActionResponse Index()
{
// Get all contacts
var contacts = Database.Query<Contact>().ToList();
return View(contacts);
}
Query with Filtering
public ActionResponse PartnerContacts()
{
var contacts = Database.Query<Contact>()
.Where(c => c.Account.Type == "Partner")
.ToList();
return View(contacts);
}
Query with Multiple Conditions
public ActionResponse SearchContacts(string name, string email)
{
var query = Database.Query<Contact>();
if (!string.IsNullOrEmpty(name))
{
query = query.Where(c => c.FirstName.Contains(name) || c.LastName.Contains(name));
}
if (!string.IsNullOrEmpty(email))
{
query = query.Where(c => c.Email.Contains(email));
}
var results = query.ToList();
return View(results);
}
Query with Sorting
public ActionResponse Index()
{
var contacts = Database.Query<Contact>()
.OrderBy(c => c.LastName)
.ThenBy(c => c.FirstName)
.ToList();
return View(contacts);
}
Query with Pagination
public ActionResponse Index(int page = 1, int pageSize = 25)
{
int skip = (page - 1) * pageSize;
var contacts = Database.Query<Contact>()
.OrderBy(c => c.LastName)
.Skip(skip)
.Take(pageSize)
.ToList();
int totalCount = Database.Query<Contact>().Count();
DataBag.CurrentPage = page;
DataBag.PageSize = pageSize;
DataBag.TotalPages = (int)Math.Ceiling((double)totalCount / pageSize);
return View(contacts);
}
Aggregations
public ActionResponse Statistics()
{
// Count records
int totalAccounts = Database.Query<Account>().Count();
// Sum values
decimal totalRevenue = Database.Query<Opportunity>()
.Where(o => o.Stage == "Closed Won")
.Sum(o => o.Amount);
// Average
decimal avgDealSize = Database.Query<Opportunity>()
.Where(o => o.Stage == "Closed Won")
.Average(o => o.Amount);
// Max/Min
decimal largestDeal = Database.Query<Opportunity>()
.Max(o => o.Amount);
DataBag.TotalAccounts = totalAccounts;
DataBag.TotalRevenue = totalRevenue;
DataBag.AvgDealSize = avgDealSize;
DataBag.LargestDeal = largestDeal;
return View();
}
Related Records
Accessing Related Records
public ActionResponse AccountDetails(string id)
{
// Retrieve account
Account account = Database.Retrieve<Account>(id);
// Get related contacts
var contacts = Database.Query<Contact>()
.Where(c => c.AccountId == id)
.OrderBy(c => c.LastName)
.ToList();
// Get related opportunities
var opportunities = Database.Query<Opportunity>()
.Where(o => o.AccountId == id)
.OrderByDescending(o => o.CreatedDate)
.ToList();
var viewModel = new AccountDetailsViewModel
{
Account = account,
Contacts = contacts,
Opportunities = opportunities
};
return View(viewModel);
}
Querying Through Relationships
public ActionResponse HighValueOpportunities()
{
var opportunities = Database.Query<Opportunity>()
.Where(o => o.Amount > 100000 && o.Account.Type == "Partner")
.OrderByDescending(o => o.Amount)
.ToList();
return View(opportunities);
}
Bulk Operations
Bulk Insert
public ActionResponse ImportContacts(List<Contact> contacts)
{
int imported = 0;
int errors = 0;
var contactsToInsert = new List<Contact>();
var validationErrors = new List<string>();
// Validate contacts (business logic only, no database calls)
foreach (var contact in contacts)
{
try
{
// Perform validation
if (string.IsNullOrWhiteSpace(contact.Email))
{
throw new Exception("Email is required");
}
contactsToInsert.Add(contact);
}
catch (Exception ex)
{
errors++;
validationErrors.Add($"Contact {contact.FirstName} {contact.LastName}: {ex.Message}");
SystemInfo.Debug($"Validation error: {ex.Message}");
}
}
// Single batch insert operation
if (contactsToInsert.Count > 0)
{
try
{
Database.Insert(contactsToInsert);
imported = contactsToInsert.Count;
}
catch (Exception ex)
{
SystemInfo.Debug($"Batch insert error: {ex.Message}");
AspxPage.AddError("An error occurred during import.");
return RedirectToAction("Index");
}
}
if (imported > 0)
{
AspxPage.AddMessage($"{imported} contacts imported successfully.");
}
if (errors > 0)
{
AspxPage.AddError($"{errors} contacts failed to import.");
}
return RedirectToAction("Index");
}
Bulk Update
public ActionResponse UpdateAccountType(string oldType, string newType)
{
var accounts = Database.Query<Account>()
.Where(a => a.Type == oldType)
.ToList();
foreach (var account in accounts)
{
account.Type = newType;
Database.Update(account);
}
AspxPage.AddMessage($"{accounts.Count} accounts updated from {oldType} to {newType}.");
return RedirectToAction("Index");
}
Advanced Query Patterns
Example 1: Complex Filter with Date Ranges
public ActionResponse OpportunityReport(DateTime startDate, DateTime endDate, string stage)
{
var query = Database.Query<Opportunity>()
.Where(o => o.CloseDate >= startDate && o.CloseDate <= endDate);
if (!string.IsNullOrEmpty(stage))
{
query = query.Where(o => o.Stage == stage);
}
var opportunities = query
.OrderBy(o => o.CloseDate)
.ToList();
decimal totalAmount = opportunities.Sum(o => o.Amount);
DataBag.StartDate = startDate;
DataBag.EndDate = endDate;
DataBag.TotalAmount = totalAmount;
DataBag.Count = opportunities.Count;
return View(opportunities);
}
Example 2: Grouping and Aggregation
public ActionResponse SalesByRegion()
{
var opportunities = Database.Query<Opportunity>()
.Where(o => o.Stage == "Closed Won")
.ToList();
// Group by region and calculate totals
var regionStats = opportunities
.GroupBy(o => o.Account.Region)
.Select(g => new RegionStats
{
Region = g.Key,
TotalRevenue = g.Sum(o => o.Amount),
DealCount = g.Count(),
AverageDealSize = g.Average(o => o.Amount)
})
.OrderByDescending(r => r.TotalRevenue)
.ToList();
return View(regionStats);
}
Error Handling
Proper error handling ensures your controllers gracefully handle exceptions and provide meaningful feedback to users.
Try-Catch Pattern
public ActionResponse Details(string id)
{
try
{
Contact contact = Database.Retrieve<Contact>(id);
if (contact == null)
{
return PageNotFound();
}
return View(contact);
}
catch (Exception ex)
{
SystemInfo.Debug($"Error retrieving contact: {ex.Message}");
return RedirectToError("An error occurred while retrieving the contact.");
}
}
Handling Specific Exceptions
[HttpPost]
public ActionResponse Create(Contact model)
{
try
{
if (ModelState.IsValid)
{
Database.Insert(model);
AspxPage.AddMessage("Contact created successfully!");
return RedirectToAction("Index");
}
return View(model);
}
catch (DuplicateRecordException)
{
AspxPage.AddError("A contact with this email already exists.");
return View(model);
}
catch (ValidationException ex)
{
AspxPage.AddError($"Validation error: {ex.Message}");
return View(model);
}
catch (Exception ex)
{
SystemInfo.Debug($"Unexpected error: {ex.Message}");
return RedirectToError("An unexpected error occurred. Please try again.");
}
}
Logging Errors
public ActionResponse ProcessBatch()
{
try
{
var contacts = Database.Query<Contact>().ToList();
foreach (var contact in contacts)
{
try
{
// Process each contact
ProcessContact(contact);
}
catch (Exception ex)
{
// Log individual failures but continue processing
SystemInfo.Debug($"Error processing contact {contact.Id}: {ex.Message}");
SystemInfo.Error(ex);
}
}
AspxPage.AddMessage("Batch processing complete.");
return RedirectToAction("Index");
}
catch (Exception ex)
{
SystemInfo.Error(ex);
return RedirectToError("Batch processing failed.");
}
}
HandleExceptionsForJson Attribute
For API controllers that return JSON, use the [HandleExceptionsForJson] attribute to ensure errors are returned in JSON format:
[HandleExceptionsForJson]
public ActionResponse GetContact(string id)
{
Contact contact = Database.Retrieve<Contact>(id);
if (contact == null)
{
throw new NotFoundException("Contact not found");
}
return Json(contact, JsonRequestBehavior.AllowGet);
}
With this attribute, any exceptions are automatically caught and returned as JSON:
{
"success": false,
"error": "Contact not found"
}
Custom Error Responses
public ActionResponse Delete(string id)
{
try
{
// Check if record has dependencies
var dependentOpportunities = Database.Query<Opportunity>()
.Where(o => o.ContactId == id)
.Count();
if (dependentOpportunities > 0)
{
return RedirectToError($"Cannot delete this contact because it has {dependentOpportunities} related opportunities. Please remove the opportunities first.");
}
Database.Delete<Contact>(id);
AspxPage.AddMessage("Contact deleted successfully.");
return RedirectToAction("Index");
}
catch (Exception ex)
{
SystemInfo.Error(ex);
return RedirectToError("An error occurred while deleting the contact.");
}
}
Practical Error Handling Examples
Example 1: File Upload with Error Handling
public ActionResponse UploadDocument(HttpPostedFileBase file)
{
try
{
if (file == null || file.ContentLength == 0)
{
AspxPage.AddError("Please select a file to upload.");
return View();
}
// Check file size (max 5MB)
if (file.ContentLength > 5 * 1024 * 1024)
{
AspxPage.AddError("File size must be less than 5MB.");
return View();
}
// Check file type
string[] allowedExtensions = { ".pdf", ".doc", ".docx", ".xls", ".xlsx" };
string extension = Path.GetExtension(file.FileName).ToLower();
if (!allowedExtensions.Contains(extension))
{
AspxPage.AddError("Invalid file type. Allowed types: PDF, Word, Excel.");
return View();
}
// Process file
byte[] fileData = new byte[file.ContentLength];
file.InputStream.Read(fileData, 0, file.ContentLength);
// Save to storage
string fileId = Storage.WriteFile(fileData, file.FileName, file.ContentType);
AspxPage.AddMessage("File uploaded successfully!");
return RedirectToAction("Index");
}
catch (Exception ex)
{
SystemInfo.Error(ex);
return RedirectToError("An error occurred during file upload.");
}
}
Example 2: External API Call with Error Handling
public ActionResponse SyncWithExternalSystem(string accountId)
{
try
{
Account account = Database.Retrieve<Account>(accountId);
if (account == null)
{
return PageNotFound();
}
// Convert account to JSON
string jsonPayload = JsonHelper.ToJson(account);
// Create HTTP request info
var requestInfo = new HttpRequestInfo
{
Url = "https://api.external-system.com/accounts",
ContentType = "application/json",
PostData = jsonPayload,
Timeout = 30000 // 30 seconds in milliseconds
};
// Call external API using HttpHelper
var response = HttpHelper.Post(requestInfo);
if (response.IsSuccessful)
{
AspxPage.AddMessage("Account synced successfully with external system.");
return RedirectToAction("Details", new { id = accountId });
}
else
{
AspxPage.AddError($"External system returned error: {response.StatusCode}");
SystemInfo.Debug($"API Error: {response.ErrorMessage}");
return RedirectToAction("Details", new { id = accountId });
}
}
catch (Exception ex)
{
SystemInfo.Error(ex);
AspxPage.AddError("An error occurred during synchronization.");
return RedirectToAction("Details", new { id = accountId });
}
}
Summary
This section covered the essential aspects of working with custom controllers in Magentrix:
✅ Action Methods - Public methods that respond to HTTP requests and return ActionResponse objects
✅ HTTP Method Handling - Using GET for displaying pages and POST for processing forms
✅ Working with Parameters - Reading URL parameters and binding POST data to models
✅ Accessing User Information - Using UserInfo and SystemInfo to access current user data
✅ Working with Cookies - Reading, writing, and removing browser cookies for state management
✅ Displaying Messages - Showing errors, warnings, and success messages to users
✅ Model Binding and Validation - Automatic form binding and using ModelState for validation
✅ Using DataBag - Passing temporary data from controllers to views
✅ Database Operations - Querying, inserting, updating, and deleting records
✅ Error Handling - Gracefully handling exceptions and providing meaningful feedback
Next Steps
Continue your learning journey with these related topics:
Quick Reference
Essential Methods
| Method/Property | Purpose |
|---|
Database.Query<T>() | Query entity records |
Database.Retrieve(id) | Get single record by ID |
Database.Insert(model) | Create new record |
Database.Update(model) | Update existing record |
Database.Delete(id) | Delete record |
UserInfo.Email | Current user's email |
UserInfo.Name | Current user's name |
UserInfo.IsPortalUser | Check if portal user |
UserInfo.Contact | Portal user's Contact record |
UserInfo.Account | Portal user's Account record |
SystemInfo.IsGuestUser | Check if user is guest |
AspxPage.GetParameter(key) | Read URL parameter |
AspxPage.GetCookie(name) | Read browser cookie |
AspxPage.SetCookie(cookie) | Write browser cookie |
AspxPage.RemoveCookie(name) | Delete browser cookie |
AspxPage.AddMessage(msg) | Show success message |
AspxPage.AddError(msg) | Show error message |
AspxPage.AddWarning(msg) | Show warning message |
ModelState.IsValid | Check if validation passed |
DataBag.PropertyName | Store temporary data |
Common Patterns
GET/POST Action Pair:
public ActionResponse Edit(string id)
{
return View(Database.Retrieve(id));
}
[HttpPost]
public ActionResponse Edit(Contact model)
{
if (ModelState.IsValid)
{
Database.Update(model);
return RedirectToAction("Index");
}
return View(model);
}
Error Handling:
try
{
// Your logic
return View();
}
catch (Exception ex)
{
EventLogProvider.Debug(ex.Message);
return RedirectToError("An error occurred.");
}
User-Specific Data:
if (UserInfo.IsPortalUser)
{
// Filter by user's account
var data = Database.Query<Opportunity>()
.Where(o => o.AccountId == UserInfo.Account.Id)
.ToList();
}
💡 You now have a solid foundation for building custom controllers! Practice these patterns and explore the next sections to master controller responses, security, and advanced techniques.