
You're staring at a canvas app that needs to display 15,000 customer records, allow filtered searching, enable bulk edits, and maintain sub-second response times. Your stakeholders want Excel-like flexibility with database reliability, and they want it yesterday. Welcome to the world where Power Apps' data controls—galleries, forms, and data tables—either make you a hero or send you spiraling into performance hell.
Most developers treat these controls as simple UI widgets. That's a mistake. Each control represents a different philosophy for handling data: galleries prioritize customization and performance, forms focus on user experience and validation, and data tables emphasize bulk operations and familiar interfaces. Understanding when and how to leverage each control, along with their underlying delegation patterns, connection architectures, and memory management strategies, separates competent app builders from true Power Apps engineers.
By the end of this deep dive, you'll architect data interfaces that scale, debug performance bottlenecks like a detective, and choose the right control pattern before you write your first formula.
What you'll learn:
You should have solid experience building canvas apps, understand Power Fx fundamentals, and be comfortable with data source connections. Familiarity with delegation concepts and basic performance optimization will help you get the most from this lesson.
Before diving into implementation details, let's establish the fundamental architectural differences between these controls and why they exist.
Galleries excel at displaying large datasets with maximum customization. They're built around virtualization—only rendering visible items plus a small buffer. This makes them capable of handling thousands of records without the memory bloat you'd see with other approaches.
The gallery's template-based architecture means every item uses the same layout and formulas, which Power Apps can optimize aggressively. Behind the scenes, galleries maintain a sophisticated caching layer that prefetches data as users scroll, manages connection pooling to avoid overwhelming your data source, and implements intelligent batching for network requests.
However, galleries require more setup for complex interactions. Want inline editing? You'll build it yourself. Need sortable columns? That's custom logic. Want row selection with bulk operations? Prepare for some formula engineering.
Forms prioritize user experience and data integrity over raw performance. They're designed around the single-record paradigm—creating, editing, or viewing one item at a time. This focus enables rich validation, automatic save state management, and sophisticated error handling that galleries and data tables can't match.
Forms automatically handle optimistic UI updates, retry logic for failed saves, and conflict resolution when multiple users edit the same record. They also provide the richest validation framework, supporting field-level validation, cross-field validation, and async validation patterns.
The trade-off is performance at scale. Forms aren't designed for bulk operations or large dataset browsing. They shine in detail views, configuration screens, and anywhere data quality matters more than quantity.
Data tables bridge the gap between galleries and forms, providing a spreadsheet-like interface that's immediately familiar to business users. They handle sorting, filtering, and basic editing without custom formulas, making them ideal for rapid prototyping and scenarios where development time is constrained.
Under the hood, data tables implement their own virtualization and caching strategies, but they're less configurable than galleries. They excel at scenarios where users need to quickly scan, sort, and edit tabular data without the complexity of custom interfaces.
The limitation is customization. Data tables look like data tables. You can't embed rich media, create custom layouts, or implement complex interaction patterns. They're powerful within their constraints but inflexible outside them.
Let's start with galleries because they're the most versatile and, when properly implemented, the highest-performing option for large datasets.
Delegation is where most Power Apps developers fail. They understand the concept—push filtering and sorting to the data source instead of pulling everything locally—but they don't understand the nuances that make delegation work in real applications.
Consider this common scenario: you're building a customer service app that needs to display support tickets. Users need to filter by status, priority, customer, and date range. Here's how most developers approach it:
// Wrong: This breaks delegation and limits to 2,000 records
Filter(
SupportTickets,
Status = StatusDropdown.Selected.Value &&
Priority = PriorityDropdown.Selected.Value &&
Created >= DateRangeStart.SelectedDate &&
Created <= DateRangeEnd.SelectedDate
)
This looks reasonable, but it has several delegation-breaking issues. Complex boolean logic, especially with multiple && operators, often doesn't delegate properly. Date comparisons with local variables can break delegation depending on your data source. And multiple filter conditions in a single Filter() function can overwhelm the delegation engine.
Here's the engineered approach:
// Right: Build delegatable filters progressively
With({
_baseFilter: Filter(SupportTickets, Status = StatusDropdown.Selected.Value),
_priorityFilter: If(
IsBlank(PriorityDropdown.Selected),
_baseFilter,
Filter(_baseFilter, Priority = PriorityDropdown.Selected.Value)
),
_dateFilter: If(
IsBlank(DateRangeStart.SelectedDate),
_priorityFilter,
Filter(_priorityFilter, Created >= DateRangeStart.SelectedDate)
)
},
If(
IsBlank(DateRangeEnd.SelectedDate),
_dateFilter,
Filter(_dateFilter, Created <= DateRangeEnd.SelectedDate)
)
)
This approach chains filters instead of combining them, which delegates more reliably. Each filter operation is simple and single-purpose. The With() function prevents repeated evaluation of complex expressions, improving both performance and readability.
But we can do better. For high-performance applications, implement a filter state management pattern:
// In App.OnStart
Set(FilterState, {
Status: Blank(),
Priority: Blank(),
DateStart: Blank(),
DateEnd: Blank(),
SearchTerm: ""
});
// Create a reusable filter function
Set(BuildTicketFilter,
Filter(
SupportTickets,
(IsBlank(FilterState.Status) || Status = FilterState.Status) &&
(IsBlank(FilterState.Priority) || Priority = FilterState.Priority) &&
(IsBlank(FilterState.DateStart) || Created >= FilterState.DateStart) &&
(IsBlank(FilterState.DateEnd) || Created <= FilterState.DateEnd) &&
(IsEmpty(FilterState.SearchTerm) ||
StartsWith(Title, FilterState.SearchTerm) ||
StartsWith(CustomerName, FilterState.SearchTerm))
)
);
// Gallery Items property
BuildTicketFilter
This pattern centralizes filter logic, makes filters easily testable, and provides a foundation for advanced features like saved filter presets and filter history.
Search functionality often becomes the performance bottleneck in gallery applications. Users expect instant results, but naive search implementations can bring apps to their knees.
The key insight is that different types of search require different strategies. Exact match searches can delegate efficiently. Fuzzy searches cannot. Text searches work well with indexed columns but poorly with computed columns. Understanding these constraints lets you build search that feels instant while staying within delegation limits.
// Implement tiered search strategy
With({
_exactMatches: If(
Len(SearchBox.Text) >= 3,
Filter(
SupportTickets,
Title = SearchBox.Text ||
TicketNumber = SearchBox.Text ||
CustomerEmail = SearchBox.Text
),
Table()
),
_startsWithMatches: If(
Len(SearchBox.Text) >= 2,
Filter(
SupportTickets,
StartsWith(Title, SearchBox.Text) ||
StartsWith(CustomerName, SearchBox.Text) ||
StartsWith(CustomerEmail, SearchBox.Text)
),
Table()
),
_containsMatches: If(
Len(SearchBox.Text) >= 4,
Filter(
SupportTickets,
SearchBox.Text in Title ||
SearchBox.Text in Description
),
Table()
)
},
Distinct(
_exactMatches,
_startsWithMatches,
_containsMatches,
ID
)
)
This tiered approach prioritizes exact matches (which are fastest), falls back to prefix matches (which delegate well), and only attempts contains searches (which are expensive) for longer search terms. The Distinct() function removes duplicates across tiers.
For even better performance, implement search debouncing to avoid overwhelming your data source:
// In SearchBox.OnChange
Set(SearchStartTime, Now());
Set(SearchTerm, Self.Text);
// Create a timer control with Duration = 500, AutoStart = false
// In SearchTimer.OnTimerEnd
If(
DateDiff(SearchStartTime, Now(), Milliseconds) >= 450,
Set(ActiveSearchTerm, SearchTerm)
);
// In SearchBox.OnChange (add this line)
Reset(SearchTimer); Start(SearchTimer);
// Gallery Items uses ActiveSearchTerm instead of SearchBox.Text
This debouncing pattern waits 500ms after the user stops typing before executing the search, dramatically reducing unnecessary API calls.
Galleries implement virtualization, but you can tune their behavior for specific use cases. The key properties are LoadingSpinner, LoadingSpinnerColor, and the often-overlooked TemplatePadding and TemplateSize.
For galleries with complex templates, memory usage can balloon quickly. Each template instance creates its own formula context, and complex formulas get evaluated for every visible item. Here's how to optimize:
// Instead of complex expressions in template controls
Label1.Text = If(
ThisItem.Priority = "High",
"🔴 " & ThisItem.Title,
If(
ThisItem.Priority = "Medium",
"🟡 " & ThisItem.Title,
"🟢 " & ThisItem.Title
)
)
// Create computed columns at the gallery level
Gallery1.Items = AddColumns(
FilteredTickets,
"DisplayTitle",
Switch(
Priority,
"High", "🔴 " & Title,
"Medium", "🟡 " & Title,
"🟢 " & Title
),
"StatusColor",
Switch(
Status,
"Open", RGBA(255, 0, 0, 1),
"In Progress", RGBA(255, 165, 0, 1),
"Closed", RGBA(0, 128, 0, 1),
RGBA(128, 128, 128, 1)
)
)
// Template controls use simple references
Label1.Text = ThisItem.DisplayTitle
Rectangle1.Fill = ThisItem.StatusColor
This pattern moves complex calculations out of the template and into the gallery's Items property, where they're calculated once per item rather than once per visible template instance.
Modern applications need to work across devices and screen sizes. Galleries provide several mechanisms for responsive behavior, but they require careful orchestration.
// Create responsive column calculations
With({
_screenWidth: App.Width,
_minItemWidth: 300,
_maxItemWidth: 500,
_padding: 20
},
{
ItemsPerRow: Max(1, Int((_screenWidth - _padding) / _minItemWidth)),
ItemWidth: Min(
_maxItemWidth,
(_screenWidth - _padding) / Max(1, Int((_screenWidth - _padding) / _minItemWidth)) - 10
)
}
)
Use this calculation in your gallery's TemplateSize and template control positioning:
// Gallery.TemplateSize
App.ResponsiveLayout.ItemWidth + 10
// Gallery.WrapCount
App.ResponsiveLayout.ItemsPerRow
// Template controls use relative positioning
Container1.Width = Parent.TemplateWidth - 20
Container1.X = 10
This creates a responsive grid that adapts to screen size while maintaining usable item dimensions.
Forms might seem straightforward—they edit records, right?—but sophisticated applications demand sophisticated form architectures. Let's explore patterns that handle complex validation, async operations, and integration with other systems.
Basic field validation is trivial in Power Apps. Real applications need cross-field validation, async validation (checking unique constraints against databases), conditional validation that changes based on user roles or data state, and validation that integrates with external systems.
Here's a validation framework that handles these scenarios:
// Create validation state management
Set(ValidationState, {
IsValidating: false,
Errors: Table(),
Warnings: Table(),
LastValidated: Blank()
});
// Validation function for email uniqueness (async)
Set(ValidateEmailAsync,
UpdateContext({_validatingEmail: true});
With({
_existingUser: LookUp(Users, Email = EmailInput.Text && ID <> Form1.LastSubmit.ID)
},
UpdateContext({_validatingEmail: false});
If(
IsBlank(_existingUser),
true,
// Add error to validation state
Set(ValidationState, Patch(ValidationState, {
Errors: Patch(ValidationState.Errors, {
Field: "Email",
Message: "Email address already exists",
Severity: "Error"
})
}));
false
)
)
);
// Cross-field validation for business rules
Set(ValidateBusinessRules,
// Manager approval required for high-value requests
If(
RequestAmount.Text > 10000 && IsBlank(ManagerApproval.Selected),
Set(ValidationState, Patch(ValidationState, {
Errors: Patch(ValidationState.Errors, {
Field: "ManagerApproval",
Message: "Manager approval required for requests over $10,000",
Severity: "Error"
})
}))
);
// Warn if deadline is less than standard processing time
If(
DateDiff(Today(), RequestDeadline.SelectedDate, Days) < 5,
Set(ValidationState, Patch(ValidationState, {
Warnings: Patch(ValidationState.Warnings, {
Field: "RequestDeadline",
Message: "Short deadline may require expedited processing",
Severity: "Warning"
})
}))
)
);
Implement validation triggers that respect user experience:
// Field-level validation on focus loss
EmailInput.OnChange = If(
Len(Self.Text) > 0 && IsMatch(Self.Text, Match.Email),
ValidateEmailAsync,
// Clear previous email validation errors
Set(ValidationState, Patch(ValidationState, {
Errors: Filter(ValidationState.Errors, Field <> "Email")
}))
);
// Form-level validation before save
SaveButton.OnSelect =
// Clear previous validation
Set(ValidationState, {
IsValidating: true,
Errors: Table(),
Warnings: Table(),
LastValidated: Now()
});
ValidateBusinessRules;
Set(ValidationState, Patch(ValidationState, {IsValidating: false}));
If(
CountRows(ValidationState.Errors) = 0,
SubmitForm(Form1),
// Show validation summary
UpdateContext({ShowValidationSummary: true})
);
Enterprise forms often need to track changes, implement auto-save, handle optimistic updates, and manage concurrent editing scenarios. Here's an architecture that handles these requirements:
// Initialize form state management
Set(FormState, {
OriginalRecord: Blank(),
CurrentRecord: Blank(),
HasUnsavedChanges: false,
LastAutoSave: Blank(),
IsAutoSaving: false,
ConflictDetected: false
});
// Track changes automatically
Form1.OnChange =
With({
_currentValues: {
Title: TitleInput.Text,
Description: DescriptionInput.Text,
Priority: PriorityDropdown.Selected,
DueDate: DueDatePicker.SelectedDate
}
},
Set(FormState, Patch(FormState, {
CurrentRecord: _currentValues,
HasUnsavedChanges: !Equals(_currentValues, FormState.OriginalRecord)
}));
// Trigger auto-save after 30 seconds of inactivity
Reset(AutoSaveTimer);
Start(AutoSaveTimer)
);
// Auto-save implementation
AutoSaveTimer.OnTimerEnd =
If(
FormState.HasUnsavedChanges && !FormState.IsAutoSaving,
Set(FormState, Patch(FormState, {IsAutoSaving: true}));
With({
_saveResult: Patch(
SupportTickets,
LookUp(SupportTickets, ID = Form1.LastSubmit.ID),
FormState.CurrentRecord
)
},
If(
IsError(_saveResult),
// Handle auto-save failure gracefully
UpdateContext({AutoSaveError: "Auto-save failed. Please save manually."}),
// Update tracking state
Set(FormState, {
OriginalRecord: FormState.CurrentRecord,
HasUnsavedChanges: false,
LastAutoSave: Now(),
IsAutoSaving: false
})
)
)
);
For responsive user experiences, implement optimistic updates that assume operations will succeed, then handle failures gracefully:
SaveButton.OnSelect =
// Show optimistic success state immediately
UpdateContext({
ShowSuccessMessage: true,
IsSaving: false
});
// Navigate away optimistically
Navigate(TicketListScreen);
// Perform actual save in background
With({
_saveOperation: Patch(
SupportTickets,
LookUp(SupportTickets, ID = Form1.LastSubmit.ID),
{
Title: TitleInput.Text,
Description: DescriptionInput.Text,
LastModified: Now(),
ModifiedBy: User()
}
)
},
If(
IsError(_saveOperation),
// Revert optimistic state and show error
UpdateContext({ShowSuccessMessage: false});
Navigate(EditTicketScreen, ScreenTransition.None);
UpdateContext({
SaveError: "Save failed: " & FirstError.Message,
ShowErrorDialog: true
}),
// Confirm successful save
Set(LastSuccessfulSave, Now())
)
);
Data tables seem simple, but achieving Excel-like performance with large datasets requires understanding their internals and optimization strategies.
Data tables have their own delegation quirks. Unlike galleries, they delegate sorting and basic filtering automatically, but they struggle with complex filters and computed columns. Here's how to work with their strengths:
// Instead of complex computed columns in the data source
DataTable1.Items = SupportTickets
// Use the built-in filtering instead of custom Filter() functions
// Configure column filters in the data table properties
// For complex filtering, pre-process the data
DataTable1.Items =
AddColumns(
Filter(
SupportTickets,
Status in ["Open", "In Progress"] // Simple, delegatable filter
),
"DaysOpen", DateDiff(Created, Now(), Days),
"IsOverdue", DueDate < Now(),
"PriorityScore", Switch(
Priority,
"High", 3,
"Medium", 2,
"Low", 1,
0
)
)
Data tables can handle large datasets, but they need tuning for optimal performance:
// Implement progressive loading for very large datasets
With({
_pageSize: 100,
_currentPage: DataTablePageState.Page
},
FirstN(
SortByColumns(
FilteredDataSource,
DataTablePageState.SortColumn,
If(DataTablePageState.SortDirection = "Ascending", Ascending, Descending)
),
_pageSize * _currentPage
)
)
Configure the data table for optimal rendering:
Data tables support custom column formatting that can dramatically improve usability:
// For date columns, use relative formatting
ThisItem.CreatedDate = DateAdd(Today(), -DateDiff(ThisItem.Created, Today(), Days), Days)
// For status columns, use color coding
ThisItem.StatusColor = Switch(
ThisItem.Status,
"Open", RGBA(220, 53, 69, 1), // Red
"In Progress", RGBA(255, 193, 7, 1), // Yellow
"Completed", RGBA(40, 167, 69, 1), // Green
RGBA(108, 117, 125, 1) // Gray
)
// For numeric columns, use conditional formatting
ThisItem.AmountColor = If(
ThisItem.Amount > ThisItem.Budget,
RGBA(220, 53, 69, 1), // Over budget - red
RGBA(40, 167, 69, 1) // Under budget - green
)
Real applications rarely use just one control type. The most powerful Power Apps combine galleries, forms, and data tables strategically. Here are patterns that create seamless user experiences.
The classic master-detail pattern uses different controls for browsing versus editing:
// Master view (Gallery for browsing with search and filters)
MasterGallery.Items =
SortByColumns(
Filter(
SupportTickets,
(IsBlank(StatusFilter.Selected) || Status = StatusFilter.Selected.Value) &&
(IsEmpty(SearchTerm) || SearchTerm in Title || SearchTerm in Description)
),
"Created",
Descending
)
// Selection management
MasterGallery.OnSelect =
Set(SelectedTicket, ThisItem);
Navigate(DetailScreen)
// Detail view (Form for editing selected record)
DetailForm.Item = SelectedTicket
DetailForm.DataSource = SupportTickets
For scenarios requiring quick edits without navigation:
// Gallery with inline edit capability
EditableGallery.Items =
AddColumns(
BaseDataSource,
"IsEditing", ThisItem.ID = EditingItemID,
"EditableTitle", If(ThisItem.ID = EditingItemID, EditTitleInput.Text, ThisItem.Title)
)
// Toggle edit mode
EditButton.OnSelect =
If(
EditingItemID = ThisItem.ID,
// Save and exit edit mode
Patch(
SupportTickets,
LookUp(SupportTickets, ID = ThisItem.ID),
{Title: EditTitleInput.Text}
);
Set(EditingItemID, Blank()),
// Enter edit mode
Set(EditingItemID, ThisItem.ID);
Set(EditTitleInput.Text, ThisItem.Title)
)
// Template shows edit controls conditionally
EditTitleInput.Visible = ThisItem.IsEditing
TitleLabel.Visible = !ThisItem.IsEditing
For scenarios requiring bulk operations across multiple records:
// Selection state management
Gallery1.OnSelect =
If(
ThisItem.ID in SelectedItems,
// Remove from selection
Set(SelectedItems, Filter(SelectedItems, Value <> ThisItem.ID)),
// Add to selection
Set(SelectedItems, Collect(SelectedItems, {Value: ThisItem.ID}))
)
// Bulk update implementation
BulkUpdateButton.OnSelect =
With({
_selectedRecords: Filter(SupportTickets, ID in SelectedItems),
_updateData: {
Status: BulkStatusDropdown.Selected.Value,
LastModified: Now(),
ModifiedBy: User()
}
},
ForAll(
_selectedRecords,
Patch(SupportTickets, ThisRecord, _updateData)
);
// Clear selection after update
Clear(SelectedItems);
UpdateContext({
BulkUpdateMessage: CountRows(_selectedRecords) & " records updated successfully"
})
)
Let's build a comprehensive customer service ticketing system that demonstrates all the concepts we've covered. This exercise will create a real-world application with advanced filtering, inline editing, bulk operations, and performance optimization.
Create a new canvas app and set up a SharePoint list called "ServiceTickets" with these columns:
Create a gallery that implements advanced filtering and search:
// App.OnStart - Initialize application state
Set(FilterState, {
Status: Blank(),
Priority: Blank(),
AssignedTo: Blank(),
DateRange: "All",
SearchTerm: ""
});
Set(ViewState, {
CurrentView: "Gallery",
SelectedItem: Blank(),
EditingItem: Blank(),
SelectedItems: Table()
});
// Main gallery Items property
With({
_baseFilter: ServiceTickets,
_statusFilter: If(
IsBlank(FilterState.Status),
_baseFilter,
Filter(_baseFilter, Status.Value = FilterState.Status)
),
_priorityFilter: If(
IsBlank(FilterState.Priority),
_statusFilter,
Filter(_statusFilter, Priority.Value = FilterState.Priority)
),
_dateFilter: Switch(
FilterState.DateRange,
"Today", Filter(_priorityFilter, DateValue(Text(Created)) = Today()),
"This Week", Filter(_priorityFilter,
Created >= DateAdd(Today(), -(Weekday(Today()) - 1), Days) &&
Created < DateAdd(Today(), 8 - Weekday(Today()), Days)
),
"This Month", Filter(_priorityFilter,
Month(Created) = Month(Today()) && Year(Created) = Year(Today())
),
_priorityFilter
),
_searchFilter: If(
IsEmpty(FilterState.SearchTerm),
_dateFilter,
Filter(
_dateFilter,
FilterState.SearchTerm in Title ||
FilterState.SearchTerm in Customer ||
FilterState.SearchTerm in Description
)
)
},
AddColumns(
_searchFilter,
"IsSelected", ID in ViewState.SelectedItems,
"IsOverdue", DueDate < Now() && !(Status.Value in ["Resolved", "Closed"]),
"DaysUntilDue", DateDiff(Now(), DueDate, Days),
"StatusColor", Switch(
Status.Value,
"Open", RGBA(220, 53, 69, 1),
"In Progress", RGBA(255, 193, 7, 1),
"Waiting", RGBA(108, 117, 125, 1),
"Resolved", RGBA(40, 167, 69, 1),
"Closed", RGBA(40, 167, 69, 0.5),
RGBA(108, 117, 125, 1)
)
)
)
Create the search interface:
// Search input OnChange
Set(SearchInputTime, Now());
Reset(SearchTimer);
Start(SearchTimer);
// Search timer (Duration: 500ms, AutoStart: false) OnTimerEnd
If(
DateDiff(SearchInputTime, Now(), Milliseconds) >= 450,
Set(FilterState, Patch(FilterState, {SearchTerm: SearchInput.Text}))
);
Add inline editing to your gallery template:
// Edit button in gallery template OnSelect
If(
ViewState.EditingItem = ThisItem.ID,
// Save changes
With({
_updateResult: Patch(
ServiceTickets,
LookUp(ServiceTickets, ID = ThisItem.ID),
{
Title: EditTitleInput.Text,
Priority: EditPriorityDropdown.Selected,
Status: EditStatusDropdown.Selected,
DueDate: EditDueDatePicker.SelectedDate
}
)
},
If(
IsError(_updateResult),
UpdateContext({ErrorMessage: "Save failed: " & FirstError.Message}),
Set(ViewState, Patch(ViewState, {EditingItem: Blank()}))
)
),
// Enter edit mode
Set(ViewState, Patch(ViewState, {EditingItem: ThisItem.ID}));
Set(EditTitleInput.Text, ThisItem.Title);
Set(EditPriorityDropdown.Selected, ThisItem.Priority);
Set(EditStatusDropdown.Selected, ThisItem.Status);
Set(EditDueDatePicker.SelectedDate, ThisItem.DueDate)
);
// Checkbox for bulk selection OnCheck
Set(ViewState, Patch(ViewState, {
SelectedItems: Collect(ViewState.SelectedItems, ThisItem.ID)
}));
// Checkbox OnUncheck
Set(ViewState, Patch(ViewState, {
SelectedItems: Filter(ViewState.SelectedItems, Value <> ThisItem.ID)
}));
Create bulk operation controls:
// Bulk status update OnSelect
With({
_selectedRecords: Filter(ServiceTickets, ID in ViewState.SelectedItems),
_updateData: {
Status: BulkStatusDropdown.Selected,
LastModified: Now()
}
},
ForAll(_selectedRecords,
Patch(ServiceTickets, ThisRecord, _updateData)
);
Set(ViewState, Patch(ViewState, {SelectedItems: Table()}));
UpdateContext({
BulkMessage: CountRows(_selectedRecords) & " tickets updated to " &
BulkStatusDropdown.Selected.Value
})
);
Implement performance tracking:
// Performance monitoring
Set(PerfCounters, {
LastFilterTime: Blank(),
FilterExecutions: 0,
AverageFilterTime: 0
});
// In gallery Items (wrap the existing formula)
With({
_startTime: Now()
},
With({
_result: /* Your existing gallery Items formula */,
_endTime: Now(),
_duration: DateDiff(_startTime, _endTime, Milliseconds)
},
Set(PerfCounters, {
LastFilterTime: _duration,
FilterExecutions: PerfCounters.FilterExecutions + 1,
AverageFilterTime: (PerfCounters.AverageFilterTime * PerfCounters.FilterExecutions + _duration) / (PerfCounters.FilterExecutions + 1)
});
_result
)
)
Test your application with various filter combinations, search terms, and bulk operations. Monitor the performance counters to understand how different operations affect responsiveness.
After building hundreds of data-driven Power Apps, certain mistakes appear repeatedly. Understanding these patterns will save you hours of debugging and performance tuning.
Mistake: Complex filters that break delegation Most developers understand that delegation pushes operations to the data source, but they don't recognize when their formulas break delegation.
// This breaks delegation and limits you to 2,000 records
Filter(
Orders,
Year(OrderDate) = 2023 &&
Status = "Pending" &&
Customer.Name = "Acme Corp" &&
Total > 1000
)
The Year() function doesn't delegate to most data sources. Lookup columns (like Customer.Name) often don't delegate. Complex boolean logic can break delegation even when individual conditions would delegate.
Solution: Test delegation systematically
// Test each condition individually
Set(TestFilter1, Filter(Orders, Status = "Pending")); // Check count
Set(TestFilter2, Filter(Orders, OrderDate >= Date(2023,1,1))); // Check count
Set(TestFilter3, Filter(Orders, Total > 1000)); // Check count
// Build delegatable version
Filter(
Orders,
Status = "Pending" &&
OrderDate >= Date(2023,1,1) &&
OrderDate < Date(2024,1,1) &&
Total > 1000
)
Always test your filters with datasets larger than 2,000 records. If you get exactly 2,000 results, delegation is failing.
Mistake: Computed columns in gallery templates
// This recalculates for every visible item, every time
Label1.Text = If(
DateDiff(ThisItem.Created, Now(), Days) > 30,
"Overdue: " & Text(ThisItem.Created, "mm/dd/yyyy"),
If(
DateDiff(ThisItem.Created, Now(), Days) > 14,
"Due Soon: " & Text(ThisItem.Created, "mm/dd/yyyy"),
Text(ThisItem.Created, "mm/dd/yyyy")
)
)
Solution: Compute columns at the gallery level
Gallery1.Items = AddColumns(
Orders,
"DisplayText",
With({_daysDiff: DateDiff(Created, Now(), Days)},
If(
_daysDiff > 30,
"Overdue: " & Text(Created, "mm/dd/yyyy"),
If(
_daysDiff > 14,
"Due Soon: " & Text(Created, "mm/dd/yyyy"),
Text(Created, "mm/dd/yyyy")
)
)
)
)
// Template just references the computed column
Label1.Text = ThisItem.DisplayText
Mistake: Validation that blocks user experience
// This validates on every keystroke, creating a terrible UX
EmailInput.OnChange = If(
!IsMatch(Self.Text, Match.Email),
UpdateContext({EmailError: "Invalid email format"}),
UpdateContext({EmailError: ""})
)
Users see error messages while they're still typing. This creates frustration and cognitive load.
Solution: Validate on meaningful events
// Validate on focus loss, not on every change
EmailInput.OnChange = UpdateContext({EmailTouched: true})
// Separate component for validation trigger
Timer_ValidationDelay.Duration = 1000
Timer_ValidationDelay.OnTimerEnd = If(
EmailTouched && Len(EmailInput.Text) > 0,
If(
!IsMatch(EmailInput.Text, Match.Email),
UpdateContext({EmailError: "Invalid email format"}),
UpdateContext({EmailError: ""})
)
)
EmailInput.OnChange = Reset(Timer_ValidationDelay); Start(Timer_ValidationDelay)
Mistake: Synchronous validation that blocks UI
// This freezes the UI while checking uniqueness
SaveButton.OnSelect =
If(
!IsEmpty(Filter(Users, Email = EmailInput.Text)),
UpdateContext({SaveError: "Email already exists"}),
SubmitForm(UserForm)
)
The Filter() operation blocks the UI thread until it completes, creating a poor user experience.
Solution: Async validation patterns
SaveButton.OnSelect =
UpdateContext({IsSaving: true});
// Use concurrent operations
ConcurrentTable(
{
ValidationResult: If(
!IsEmpty(Filter(Users, Email = EmailInput.Text)),
{Valid: false, Message: "Email already exists"},
{Valid: true, Message: ""}
)
}
);
If(
ValidationResult.Valid,
SubmitForm(UserForm),
UpdateContext({SaveError: ValidationResult.Message})
);
UpdateContext({IsSaving: false})
Mistake: Overloading data tables with computed columns
// This kills performance with complex calculations
DataTable1.Items = AddColumns(
AddColumns(
AddColumns(
LargeDataset,
"ComputedValue1", /* complex calculation */
),
"ComputedValue2", /* another complex calculation */
),
"ComputedValue3", /* yet another calculation */
)
Each AddColumns() creates a new iteration over the dataset. With large datasets, this becomes exponentially expensive.
Solution: Single AddColumns with multiple computed values
DataTable1.Items = AddColumns(
LargeDataset,
"ComputedValue1", /* calculation 1 */,
"ComputedValue2", /* calculation 2 */,
"ComputedValue3", /* calculation 3 */
)
Or better yet, compute values in your data source if possible (calculated columns in SharePoint, computed columns in SQL, etc.).
Mistake: Creating memory leaks with global variables
// This accumulates data in global collections
Button1.OnSelect = Collect(GlobalData, LargeDataset)
Global collections persist for the entire app session. Repeatedly collecting large datasets causes memory bloat.
Solution: Manage collection lifecycle
// Clear before collecting
Button1.OnSelect =
Clear(GlobalData);
Collect(GlobalData, FirstN(LargeDataset, 100)) // Limit data volume
Mistake: Nested With() statements that duplicate data
// This creates multiple copies of large datasets in memory
With({_data1: LargeDataset},
With({_data2: Filter(_data1, Condition1)},
With({_data3: AddColumns(_data2, "NewColumn", Calculation)},
/* use _data3 */
)
)
)
Solution: Chain operations efficiently
With({
_filteredAndComputed: AddColumns(
Filter(LargeDataset, Condition1),
"NewColumn", Calculation
)
},
/* use _filteredAndComputed */
)
When things go wrong, systematic debugging saves time:
Create debug information displays:
// Debug label showing delegation status
DebugLabel.Text = "Gallery Items: " & CountRows(Gallery1.AllItems) &
" | Visible: " & CountRows(Gallery1.Items) &
" | Delegation: " & If(CountRows(Gallery1.AllItems) = 2000, "BROKEN", "OK")
Log performance metrics:
// Performance tracking in App.OnStart
Set(PerfLog, Table({Timestamp: Now(), Operation: "App Start", Duration: 0}));
// In expensive operations
With({_start: Now()},
/* your operation */;
Collect(PerfLog, {
Timestamp: Now(),
Operation: "Gallery Filter",
Duration: DateDiff(_start, Now(), Milliseconds)
})
)
Create test data generators:
// Generate test data for performance testing
ForAll(
Sequence(1000),
Collect(TestTickets, {
ID: Value,
Title: "Test Ticket " & Text(Value),
Status: Index(["Open", "In Progress", "Closed"], Mod(Value, 3) + 1),
Created: DateAdd(Now(), -Rand() * 365, Days),
Priority: Index(["Low", "Medium", "High"], Mod(Value, 3) + 1)
})
)
Pro Tip: Always test with realistic data volumes. An app that works with 50 test records might fail catastrophically with 5,000 production records.
You've now mastered the deep architecture of Power Apps data controls—galleries, forms, and data tables. You understand their performance characteristics, delegation strategies, and integration patterns. More importantly, you can diagnose performance issues, implement advanced features like inline editing and bulk operations, and choose the right control for each scenario.
The key insights to remember:
Your next steps depend on your specific use cases:
For large-scale enterprise applications, focus on advanced delegation patterns, performance monitoring, and error handling strategies. Study how to integrate with Azure services, implement offline capabilities, and design for governance at scale.
For complex business processes, dive deeper into form validation frameworks, workflow integration, and approval patterns. Learn how to build reusable validation components and integrate with Power Automate for complex business logic.
For data-intensive scenarios, explore advanced data modeling techniques, caching strategies, and integration with Power BI for analytics. Understand how to optimize for different data sources and implement effective data synchronization patterns.
The foundation you've built here will support whatever direction you choose. These aren't just controls—they're the building blocks of sophisticated business applications that scale, perform, and delight users.
Learning Path: Canvas Apps 101