Power Apps Model-Driven Apps - Building Enterprise Solutions
Model-driven apps in Power Apps take a fundamentally different approach from canvas apps. Instead of designing pixel-by-pixel, you define the data model and let the platform generate a responsive, consistent UI. This approach is ideal for data-heavy enterprise applications. Let me walk you through building effective model-driven apps.
When to Choose Model-Driven Apps
Model-driven apps excel when you need:
- Complex data relationships - Multiple related tables
- Consistent UX - Standard navigation and forms
- Enterprise features - Business process flows, security roles
- Rapid development - Less UI design, more data modeling
- Desktop-first - Primary use on larger screens
Understanding the Architecture
Model-driven apps consist of:
- Site Map - Navigation structure
- Tables - Data stored in Dataverse
- Forms - Data entry and viewing
- Views - Data lists and grids
- Business Process Flows - Guided processes
- Dashboards - Data visualizations
Creating Your App
Step 1: Define the Site Map
<?xml version="1.0" encoding="utf-8"?>
<SiteMap>
<Area Id="ProjectManagement" Title="Project Management">
<Group Id="Projects" Title="Projects">
<SubArea Id="nav_projects"
Entity="cr_project"
Title="Projects"
Icon="/WebResources/icon_project.svg" />
<SubArea Id="nav_tasks"
Entity="cr_task"
Title="Tasks" />
</Group>
<Group Id="Resources" Title="Resources">
<SubArea Id="nav_team"
Entity="cr_teammember"
Title="Team Members" />
<SubArea Id="nav_resources"
Entity="cr_resource"
Title="Resources" />
</Group>
<Group Id="Reports" Title="Reports">
<SubArea Id="nav_dashboard"
DefaultDashboard="ProjectDashboard"
Title="Dashboard" />
</Group>
</Area>
</SiteMap>
Step 2: Design Main Form
Form: Project Main Form
Type: Main
Tabs:
- Name: General
Sections:
- Name: Project Information
Columns: 2
Fields:
- cr_name (Required)
- cr_status
- cr_startdate
- cr_enddate
- cr_budget
- cr_customerid (Lookup)
- Name: Description
Columns: 1
Fields:
- cr_description (Full Width, 4 rows)
- Name: Tasks
Sections:
- Name: Related Tasks
SubGrid:
Entity: cr_task
View: Active Tasks
AllowQuickCreate: true
- Name: Team
Sections:
- Name: Team Members
SubGrid:
Entity: cr_teammember
View: Project Team
AllowAddExisting: true
- Name: Documents
Sections:
- Name: Related Documents
Control: SharePoint Document Grid
Step 3: Configure Views
View: Active Projects
Columns:
- cr_name (Primary, Link)
- cr_status (Width: 100)
- cr_customerid (Width: 150)
- cr_startdate (Width: 100)
- cr_enddate (Width: 100)
- cr_budget (Width: 100)
- ownerid (Width: 150)
Filter:
- cr_statecode equals 0 (Active)
- cr_status not equals Completed
Sort:
- cr_enddate Ascending
View: My Active Projects
Inherits: Active Projects
Additional Filter:
- ownerid equals Current User
Business Process Flows
Create guided experiences for complex processes:
Business Process Flow: Project Lifecycle
Entity: cr_project
Stages:
- Name: Initiation
Steps:
- cr_name (Required)
- cr_customerid (Required)
- cr_description
Exit Condition: cr_status = Initiated
- Name: Planning
Steps:
- cr_startdate (Required)
- cr_enddate (Required)
- cr_budget (Required)
Exit Condition: Budget Approved = Yes
- Name: Execution
Steps:
- cr_progress (Required)
- cr_actualhours
Exit Condition: Progress >= 100%
- Name: Closure
Steps:
- cr_lessonslearned
- cr_customersignoff (Required)
Exit Condition: Customer Signoff = Yes
Business Rules in Forms
Create no-code form logic:
Business Rule: Budget Validation
Scope: Form
Conditions:
IF cr_status = "In Progress"
AND cr_budget contains data
Actions:
- Set cr_budget Required Level: Required
- Show Error on cr_budget if cr_budget < 1000:
"Budget must be at least $1,000 for active projects"
Business Rule: Date Validation
Scope: Form
Conditions:
IF cr_enddate contains data
AND cr_startdate contains data
Actions:
- Show Error on cr_enddate if cr_enddate <= cr_startdate:
"End date must be after start date"
JavaScript Customizations
For complex scenarios, add JavaScript:
// Project form script
var ProjectForm = (function () {
"use strict";
function onLoad(executionContext) {
var formContext = executionContext.getFormContext();
// Register event handlers
formContext.getAttribute("cr_status").addOnChange(onStatusChange);
formContext.getAttribute("cr_budget").addOnChange(calculateBudgetVariance);
// Initial setup
setFieldVisibility(formContext);
loadRelatedData(formContext);
}
function onStatusChange(executionContext) {
var formContext = executionContext.getFormContext();
var status = formContext.getAttribute("cr_status").getValue();
if (status === 100000003) { // Completed
// Lock budget field
formContext.getControl("cr_budget").setDisabled(true);
// Show completion section
formContext.ui.tabs.get("tab_completion").setVisible(true);
// Require lessons learned
formContext.getAttribute("cr_lessonslearned")
.setRequiredLevel("required");
}
}
function calculateBudgetVariance(executionContext) {
var formContext = executionContext.getFormContext();
var budget = formContext.getAttribute("cr_budget").getValue() || 0;
var actualCost = formContext.getAttribute("cr_actualcost").getValue() || 0;
var variance = budget - actualCost;
var variancePercent = budget > 0 ? (variance / budget) * 100 : 0;
formContext.getAttribute("cr_budgetvariance").setValue(variance);
formContext.getAttribute("cr_variancepercent").setValue(variancePercent);
// Visual indicator
var varianceControl = formContext.getControl("cr_budgetvariance");
if (variance < 0) {
varianceControl.setNotification("Project is over budget!", "variance_warning");
} else {
varianceControl.clearNotification("variance_warning");
}
}
async function loadRelatedData(formContext) {
var projectId = formContext.data.entity.getId().replace(/[{}]/g, "");
if (!projectId) return;
try {
// Get task summary
var tasks = await Xrm.WebApi.retrieveMultipleRecords(
"cr_task",
`?$filter=_cr_projectid_value eq ${projectId}&$select=cr_estimatedhours,cr_actualhours,cr_status`
);
var totalEstimated = 0;
var totalActual = 0;
var completedTasks = 0;
tasks.entities.forEach(function (task) {
totalEstimated += task.cr_estimatedhours || 0;
totalActual += task.cr_actualhours || 0;
if (task.cr_status === 100000003) completedTasks++;
});
// Update summary fields
formContext.getAttribute("cr_totalestimatedhours").setValue(totalEstimated);
formContext.getAttribute("cr_totalactualhours").setValue(totalActual);
formContext.getAttribute("cr_taskcompletionrate")
.setValue(tasks.entities.length > 0
? (completedTasks / tasks.entities.length) * 100
: 0);
} catch (error) {
console.error("Error loading related data:", error);
}
}
function onSave(executionContext) {
var formContext = executionContext.getFormContext();
var saveEvent = executionContext.getEventArgs();
// Custom validation
var endDate = formContext.getAttribute("cr_enddate").getValue();
var startDate = formContext.getAttribute("cr_startdate").getValue();
if (endDate && startDate && endDate < startDate) {
saveEvent.preventDefault();
formContext.ui.setFormNotification(
"End date cannot be before start date",
"ERROR",
"date_validation"
);
return;
}
formContext.ui.clearFormNotification("date_validation");
}
return {
onLoad: onLoad,
onSave: onSave,
onStatusChange: onStatusChange
};
})();
Register the script:
Form Events:
OnLoad: ProjectForm.onLoad
OnSave: ProjectForm.onSave
Libraries:
- cr_projectform.js
Custom Commands (Command Bar)
Add custom buttons using Ribbon Workbench or Power Fx:
// Custom command action
function approveProject(selectedItems) {
var projectId = selectedItems[0].Id;
Xrm.Utility.confirmDialog(
"Are you sure you want to approve this project?",
function () {
// Call custom action
var request = {
entity: {
entityType: "cr_project",
id: projectId
},
getMetadata: function () {
return {
boundParameter: "entity",
operationType: 0,
operationName: "cr_ApproveProject"
};
}
};
Xrm.WebApi.online.execute(request).then(
function (response) {
Xrm.Navigation.openAlertDialog({
text: "Project approved successfully!"
});
// Refresh the form
Xrm.Page.data.refresh();
},
function (error) {
Xrm.Navigation.openAlertDialog({
text: "Error: " + error.message
});
}
);
}
);
}
Dashboard Configuration
Create interactive dashboards:
Dashboard: Project Overview
Type: Interactive
Layout: 3 Column
Charts:
- Name: Projects by Status
Type: Pie Chart
Entity: cr_project
Category: cr_status
Series: COUNT(cr_projectid)
- Name: Budget vs Actual
Type: Bar Chart
Entity: cr_project
Category: cr_name
Series:
- cr_budget
- cr_actualcost
Views:
- Name: My Active Projects
Entity: cr_project
View: My Active Projects
- Name: Overdue Tasks
Entity: cr_task
View: Overdue Tasks
Interactive Filters:
- cr_status
- cr_priority
- ownerid
- cr_customerid
Best Practices
- Design data model first - Forms and views follow the model
- Use business rules before JavaScript
- Leverage security roles - Built-in row-level security
- Create meaningful views - Users rely on views heavily
- Use business process flows for guided experiences
- Test on mobile - Model-driven apps are responsive
- Use solutions for ALM and deployment
Conclusion
Model-driven apps provide a powerful framework for building enterprise applications rapidly. By focusing on data modeling and leveraging the platform’s built-in capabilities, you can create sophisticated business applications with consistent UX and robust features. The combination of no-code business rules with pro-code extensibility makes model-driven apps suitable for a wide range of enterprise scenarios.