Table of Contents
Part 5: Testing and Verification
Table of contents
- Part 5: Testing and Verification
Learning Objectives
By the end of this part, you will:
- ✅ Test complete CRUD workflow systematically
- ✅ Verify navigation between pages
- ✅ Validate data binding functionality
- ✅ Check database persistence
- ✅ Review complete application architecture
- ✅ Understand testing strategies for MAUI apps
- ✅ Explore extension challenges for advanced learning
- ✅ Reflect on architectural patterns and best practices
Estimated time: 30 minutes
5.1: Complete CRUD Workflow Testing
Let’s systematically test all Create, Read, Update, and Delete operations.
Setup: Clean Environment
Before testing, ensure a clean state:
1
2
3
4
5
6
7
8
# Navigate to StarterApp
cd StarterApp
# Build application
dotnet build
# Run application
dotnet run
Testing approach: Follow each test in order. Don’t skip steps - each builds on the previous.
Test 1: Create Note
Objective: Verify note creation with all fields.
Steps:
- Launch app
- App should open to NotesPage
- Title shows “My Notes”
- If empty, shows empty state: “No notes yet”
- Tap “+” button (toolbar)
- Navigates to NotePage
- Title shows “New Note”
- All fields empty except importance (defaults to Normal)
- Category picker populated with categories
- Fill in note details:
- Title: “Team Meeting Notes”
- Category: Select “Work”
- Importance: Select “High”
- Content: “Discussed Q1 goals and timeline”
- Tap “Create” button
- No error messages appear
- Navigates back to NotesPage
- Loading spinner appears briefly
- Verify note appears in list:
- Note “Team Meeting Notes” visible
- Blue “Work” badge displayed
- ⬆️ icon shows (high importance)
- Content preview shows
- Today’s date shown
Expected Result: ✅ Note created and visible in list
If test fails:
- Check Output window for exceptions
- Verify database connection
- Check
CreateNoteAsyncin repository - Ensure
SaveChangesAsyncis called
Test 2: Read Notes List
Objective: Verify list displays all notes with correct data.
Steps:
- Create multiple notes with different properties:
- “Grocery Shopping” - Shopping category, Normal importance
- “Finish Assignment” - Study category, High importance
- “Workout Plan” - Personal category, Low importance
- Verify list display:
- All 4 notes visible (Team Meeting + 3 new)
- Notes ordered by most recent first
- Category badges show correct colors
- Importance icons correct (⬆️ for high, ⬇️ for low)
- Content previews truncated if long
- Dates format correctly
- Test pull-to-refresh:
- Swipe down on list
- Spinner appears
- List refreshes
- All notes still visible
Expected Result: ✅ All notes display with correct formatting
If test fails:
- Check
GetAllNotesAsyncincludes.Include(n => n.Category) - Verify
OrderByDescending(n => n.UpdatedAt) - Check ObservableCollection updates in ViewModel
Test 3: Filter by Category
Objective: Verify category filtering works correctly.
Steps:
- Verify “All Categories” selected by default
- All 4 notes visible
- Select “Work” category:
- List updates automatically
- Only “Team Meeting Notes” visible
- Other notes hidden
- Select “Study” category:
- Only “Finish Assignment” visible
- Select “Shopping” category:
- Only “Grocery Shopping” visible
- Select “Personal” category:
- Only “Workout Plan” visible
- Select “All Categories” again:
- All 4 notes visible again
Expected Result: ✅ Filtering works correctly for all categories
If test fails:
- Check
OnSelectedCategoryIdChangedpartial method - Verify repository filtering logic
- Check if
LoadNotesAsyncis called on filter change
Test 4: Read Single Note (Detail View)
Objective: Verify editing page loads note correctly.
Steps:
- Tap “Team Meeting Notes” in list
- Navigates to NotePage
- Title shows “Edit Note”
- URL includes
?id=1(or appropriate ID)
- Verify all fields populated:
- Title: “Team Meeting Notes” ✓
- Category: “Work” selected ✓
- Importance: “High” selected ✓
- Content: Full text visible ✓
- Delete button: Visible at bottom ✓
- Tap “Cancel” button
- Navigates back to NotesPage
- No changes saved
- Note unchanged in list
Expected Result: ✅ Note loads correctly with all data
If test fails:
- Check
GetNoteByIdAsyncin repository - Verify
InitializeAsyncin NoteViewModel - Check query parameter parsing in NotePage.xaml.cs
Test 5: Update Existing Note
Objective: Verify note updates persist correctly.
Steps:
- Tap “Grocery Shopping” in list
- Edit page opens
- Modify all fields:
- Change title to “Weekly Grocery Shopping”
- Change category to “Personal”
- Change importance to “High”
- Add to content: “Don’t forget milk!”
- Tap “Update” button
- No error messages
- Navigates back to list
- Loading spinner appears briefly
- Verify changes in list:
- Note title now “Weekly Grocery Shopping” ✓
- Category badge changed (different color) ✓
- ⬆️ icon now visible (high importance) ✓
- Updated date changed to today ✓
- Tap note again to verify persistence:
- All changes still present
- Content includes “Don’t forget milk!”
Expected Result: ✅ All updates saved and persist
If test fails:
- Check
UpdateNoteAsyncin repository - Verify
UpdatedAttimestamp set - Ensure
SaveChangesAsynccalled after modifications
Test 6: Delete Note (From Detail Page)
Objective: Verify deletion from detail page works.
Steps:
- Tap “Workout Plan” in list
- Edit page opens
- Scroll to bottom
- Red “Delete Note” button visible
- Tap “Delete Note”
- Confirmation dialog appears
- Message: “Are you sure you want to delete this note?”
- Two buttons: “Delete” and “Cancel”
- Tap “Cancel”
- Dialog closes
- Note NOT deleted
- Still on edit page
- Tap “Delete Note” again
- Dialog appears again
- Tap “Delete”
- Dialog closes
- Navigates back to NotesPage
- Note removed from list
- Verify deletion persisted:
- Close app completely
- Restart app
- “Workout Plan” still gone
Expected Result: ✅ Note deleted and removal persists
If test fails:
- Check
DeleteNoteAsyncin repository - Verify
Remove()andSaveChangesAsynccalled - Check navigation after deletion
Test 7: Delete Note (Swipe-to-Delete)
Objective: Verify swipe gesture deletion works.
Steps:
-
In NotesPage, find “Finish Assignment”
- Swipe left on the note
- Red delete button appears on right side
- Tap red “Delete” button
- Confirmation dialog appears
- Tap “Cancel”
- Note NOT deleted
- Swipe area closes
-
Swipe left again
-
Tap “Delete” button
- Tap “Delete” in confirmation
- Note immediately removed from list
- No navigation (stays on NotesPage)
- Verify deletion persisted:
- Pull-to-refresh
- Note still gone
Expected Result: ✅ Swipe-to-delete works and persists
Platform differences: Swipe gestures may feel different on Android vs iOS vs Windows. All should work but with platform-specific animations.
5.2: Navigation Testing
Let’s verify all navigation paths work correctly.
Test 8: Navigation Flow
Objective: Verify all navigation routes and back navigation.
Navigation map:
1
2
3
4
5
6
7
8
NotesPage (main page)
├─> NotePage (create new) → back to NotesPage
├─> NotePage (edit existing) → back to NotesPage
└─> AboutPage (flyout menu)
AppShell Flyout Menu
├─> Notes (NotesPage)
└─> About (AboutPage)
Steps:
- Test flyout menu:
- Tap hamburger menu (≡) or swipe from left
- Flyout menu opens
- See “Notes” and “About” items
- Navigate to About page:
- Tap “About” in flyout
- AboutPage loads
- Shows app information
- Navigate back to Notes:
- Tap “Notes” in flyout
- NotesPage loads
- All notes still visible
- Test create navigation:
- Tap “+” button
- NotePage loads for creation
- Tap “Cancel”
- Back to NotesPage (no note created)
- Test edit navigation:
- Tap a note
- NotePage loads for editing
- Tap device back button (Android) or gesture (iOS)
- Back to NotesPage (no changes saved)
- Test save navigation:
- Tap a note
- Make changes
- Tap “Update”
- Automatically navigates to NotesPage
- Changes visible
Expected Result: ✅ All navigation paths work correctly
If test fails:
- Check route registration in AppShell.xaml.cs
- Verify
Shell.Current.GoToAsync()calls - Check NotePage query parameter parsing
5.3: Data Binding Verification
Let’s verify data binding works in both directions.
Test 9: Two-Way Data Binding
Objective: Verify UI ↔ ViewModel synchronization.
Test A: Property Updates
-
Open any note for editing
- Modify title in Entry field
- Type: “Updated Title”
- ViewModel
Titleproperty updates automatically - No explicit code needed
- Clear title completely
- Entry shows placeholder “Enter note title”
- Tap “Update”
- Error message appears: “Title is required”
- ViewModel validation works ✓
Test B: Command Execution
- Verify buttons enabled/disabled:
- When
IsBusy = false: Buttons enabled - Tap “Update”
- While saving:
IsBusy = true - Buttons disabled (can’t double-tap)
- After save:
IsBusy = false, buttons enabled again
- When
Test C: Picker Binding
- Open note for editing
- Change category picker:
- Select different category
- ViewModel
SelectedCategoryIdupdates - Save note
- Correct category badge shows in list ✓
Expected Result: ✅ All bindings work bidirectionally
If test fails:
- Check
[ObservableProperty]on ViewModel properties - Verify
{Binding PropertyName}in XAML - Ensure
BindingContextset correctly in code-behind
Test 10: ObservableCollection Updates
Objective: Verify list updates automatically when data changes.
Steps:
-
Have NotesPage open with 2+ notes visible
- Create new note:
- Tap “+”
- Fill form
- Tap “Create”
- Observe: New note appears in list WITHOUT manual refresh
- ObservableCollection automatically notified UI ✓
- Delete note via swipe:
- Swipe and delete
- Observe: Note disappears immediately
- No delay, no manual refresh needed ✓
- Edit note:
- Tap note
- Change title
- Tap “Update”
- Observe: List shows updated title immediately ✓
Expected Result: ✅ UI updates automatically via ObservableCollection
Key insight: ObservableCollection<Note> vs List<Note>
1
2
3
4
5
// ❌ List<Note> - UI doesn't update automatically
Notes.Add(newNote); // UI doesn't know
// ✅ ObservableCollection<Note> - UI updates automatically
Notes.Add(newNote); // UI notified and updates
5.4: Database Persistence
Let’s verify data actually saves to PostgreSQL.
Test 11: Application Restart Persistence
Objective: Verify data survives app restart.
Steps:
- Create unique test note:
- Title: “Persistence Test - [Current Time]”
- Add specific content you’ll recognize
- Save note
- Close application COMPLETELY:
- Don’t just minimize
- Actually terminate the process
- On Android: Swipe away from recent apps
- On Windows: Close window
- On macOS: Cmd+Q
-
Wait 10 seconds (ensure process terminated)
- Restart application:
- Launch app fresh
- NotesPage loads
- Verify test note still exists:
- Find “Persistence Test” note
- Tap to view details
- All data intact (title, content, category, importance)
- Modify test note:
- Change something
- Save
- Restart again:
- Close app
- Reopen
- Verify changes persisted
Expected Result: ✅ All data persists across restarts
If test fails:
- Check PostgreSQL container is running:
docker ps | grep postgres - Verify connection string in appsettings.json
- Check
SaveChangesAsync()is called in repository - Query database directly to debug
Test 12: Direct Database Verification
Objective: Verify data in database matches UI.
Prerequisites: PostgreSQL client (from dev-environment tutorial)
Steps:
- Create note with specific data in app:
- Title: “Database Verification Test”
- Category: “Work”
- Importance: “High”
- Content: “This is a verification test”
- Query database directly:
1
2
3
4
5
6
7
-- Connect to PostgreSQL
docker exec -it postgres-container psql -U student_user -d starterapp
-- Query notes
SELECT id, title, importance, category_id, created_at, updated_at
FROM notes
WHERE title = 'Database Verification Test';
Expected output:
1
2
3
id | title | importance | category_id | created_at | updated_at
---+---------------------------+------------+-------------+--------------------+-------------------
5 | Database Verification Test| 2 | 2 | 2026-02-11 10:30:00| 2026-02-11 10:30:00
- Verify field mappings:
importance = 2→ High (enum value) ✓category_id = 2→ Work category ✓- Timestamps in UTC ✓
- Query with join to see category name:
1
2
3
4
SELECT n.id, n.title, n.importance, c.name as category_name
FROM notes n
LEFT JOIN categories c ON n.category_id = c.id
WHERE n.title = 'Database Verification Test';
Expected output:
1
2
3
id | title | importance | category_name
---+---------------------------+------------+--------------
5 | Database Verification Test| 2 | Work
- Verify relationships work:
1
2
3
4
5
6
7
8
-- Get all notes in Work category
SELECT COUNT(*) FROM notes WHERE category_id = 2;
-- Get category with note count
SELECT c.name, COUNT(n.id) as note_count
FROM categories c
LEFT JOIN notes n ON c.id = n.category_id
GROUP BY c.name;
Expected Result: ✅ Database data matches UI exactly
If test fails:
- Check migrations applied:
SELECT * FROM "__EFMigrationsHistory"; - Verify foreign keys:
\d notesto see constraints - Check enum values match definition
5.5: Architecture Review
Let’s review the complete application architecture you’ve built.
Complete Architecture Diagram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
┌─────────────────────────────────────────────────────────────┐
│ MAUI APPLICATION │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PRESENTATION LAYER │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ NotesPage │ │ NotePage │ │ AboutPage │ │ │
│ │ │ (XAML) │ │ (XAML) │ │ (XAML) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │
│ │ │ Data Binding │ │ │
│ │ ┌──────▼──────┐ ┌──────▼──────┐ │ │
│ │ │NotesViewModel│ │ NoteViewModel│ │ │
│ │ │ │ │ │ │ │
│ │ │ - Commands │ │ - Commands │ │ │
│ │ │ - Properties│ │ - Properties│ │ │
│ │ │ - IsBusy │ │ - IsBusy │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │
│ └─────────┼─────────────────┼───────────────────────────┘ │
│ │ │ │
│ │ Dependency Injection │
│ │ │ │
└────────────┼─────────────────┼───────────────────────────────┘
│ │
┌────────────▼─────────────────▼───────────────────────────────┐
│ BUSINESS LOGIC LAYER │
│ (StarterApp.Database Library) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ INoteRepository Interface │ │
│ │ - GetAllNotesAsync(categoryId?) │ │
│ │ - GetNoteByIdAsync(id) │ │
│ │ - CreateNoteAsync(note) │ │
│ │ - UpdateNoteAsync(note) │ │
│ │ - DeleteNoteAsync(id) │ │
│ │ - GetAllCategoriesAsync() │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴────────────────┐ │
│ │ │ │
│ ┌──────▼──────────┐ ┌────────▼──────────┐ │
│ │ NoteRepository │ │ ApiNoteRepository │ │
│ │ (Local DB) │ │ (Future) │ │
│ └──────┬──────────┘ └───────────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────────┐ │
│ │ AppDbContext (EF Core) │ │
│ │ │ │
│ │ DbSet<Note> DbSet<Category> │ │
│ │ - Relationships │ │
│ │ - Constraints │ │
│ │ - Indexes │ │
│ │ - Seed Data │ │
│ └──────┬──────────────────────────────────────────────┘ │
│ │ Npgsql Provider │
└─────────┼──────────────────────────────────────────────────┘
│
┌─────────▼──────────────────────────────────────────────────┐
│ DATA LAYER │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ │ │
│ │ Tables: │ │
│ │ - notes (title, content, importance, timestamps) │ │
│ │ - categories (name, color, description) │ │
│ │ - __EFMigrationsHistory (migration tracking) │ │
│ │ │ │
│ │ Relationships: │ │
│ │ - notes.category_id → categories.id (FK) │ │
│ │ - OnDelete: SetNull │ │
│ │ │ │
│ │ Indexes: │ │
│ │ - notes.category_id (faster filtering) │ │
│ │ - notes.created_at (faster sorting) │ │
│ │ - categories.name UNIQUE (no duplicates) │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Data Flow: Creating a Note
Let’s trace what happens when a user creates a note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
USER INTERACTION
└─> Taps "+" button in NotesPage
└─> Command: AddNoteCommand
└─> Navigation: Shell.GoToAsync("note")
NAVIGATION SYSTEM
└─> Shell resolves route "note" → NotePage
└─> DI Container provides NoteViewModel
└─> NotePage.OnAppearing()
└─> NoteViewModel.InitializeAsync(noteId: null)
VIEWMODEL INITIALIZATION
└─> NoteViewModel.InitializeAsync()
├─> Sets IsBusy = true (shows spinner)
├─> Calls _repository.GetAllCategoriesAsync()
│ └─> Repository queries database
│ └─> Returns List<Category>
├─> Updates Categories property
├─> UI updates via data binding
└─> Sets IsBusy = false (hides spinner)
USER FILLS FORM
├─> Types title → Title property updates
├─> Selects category → SelectedCategoryId updates
├─> Selects importance → Importance updates
└─> Types content → Content property updates
USER TAPS "CREATE"
└─> Command: SaveCommand
└─> NoteViewModel.SaveAsync()
├─> Validates Title (required)
├─> Validates Content (required)
├─> Sets IsBusy = true
├─> Creates Note object from properties
└─> Calls _repository.CreateNoteAsync(note)
REPOSITORY (DATA ACCESS)
└─> NoteRepository.CreateNoteAsync(note)
├─> Sets note.CreatedAt = DateTime.UtcNow
├─> Sets note.UpdatedAt = DateTime.UtcNow
├─> _context.Notes.Add(note)
├─> await _context.SaveChangesAsync()
│ └─> EF Core generates SQL:
│ INSERT INTO notes (title, content, category_id, importance, created_at, updated_at)
│ VALUES ($1, $2, $3, $4, $5, $6) RETURNING id;
├─> Loads note.Category navigation property
└─> Returns note with generated ID
DATABASE
└─> PostgreSQL executes INSERT
├─> Generates new ID (auto-increment)
├─> Stores all fields
├─> Returns ID to EF Core
└─> Data persisted to disk
BACK TO VIEWMODEL
└─> SaveAsync continues
├─> Repository returns created note
├─> No errors occurred
├─> Sets IsBusy = false
└─> Navigates: Shell.GoToAsync("..")
NAVIGATION BACK TO LIST
└─> NotesPage.OnAppearing()
└─> NotesViewModel.InitializeAsync()
└─> Calls LoadNotesAsync()
└─> _repository.GetAllNotesAsync()
└─> Returns all notes (including new one)
UI UPDATE
└─> ObservableCollection<Note> updated
├─> Notes.Clear()
├─> Notes.Add(each note)
└─> UI automatically refreshes
└─> New note appears in list!
Total time: ~200-500ms (depending on database)
5.6: Extension Challenges
Ready to level up? Try these challenges to deepen your understanding.
Challenge 1: Add Search Functionality ⭐
Difficulty: Beginner
Goal: Add search bar to filter notes by title/content.
Requirements:
- Add SearchBar control to NotesPage
- Create SearchText property in NotesViewModel
- Filter notes in real-time as user types
- Search should check both title and content
- Combine with category filter
Hints:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In repository
public async Task<List<Note>> SearchNotesAsync(string searchTerm, int? categoryId = null)
{
var query = _context.Notes.Include(n => n.Category).AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(n =>
n.Title.Contains(searchTerm) ||
n.Content.Contains(searchTerm));
}
// Apply category filter...
// Order by...
}
Learning outcome: Working with dynamic queries
Challenge 2: Soft Delete ⭐⭐
Difficulty: Intermediate
Goal: Don’t actually delete notes; mark them as deleted and hide from UI.
Requirements:
- Add
IsDeletedboolean property to Note model - Create migration for new column
- Update repository to filter out deleted notes by default
- Add “Trash” view to see deleted notes
- Add “Restore” functionality
- Add “Permanently Delete” option
Hints:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In Note model
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
// In repository
public async Task<List<Note>> GetAllNotesAsync(bool includeDeleted = false)
{
var query = _context.Notes.Include(n => n.Category);
if (!includeDeleted)
{
query = query.Where(n => !n.IsDeleted);
}
return await query.OrderByDescending(n => n.UpdatedAt).ToListAsync();
}
Learning outcome: Schema evolution, query filtering
Challenge 3: Many-to-Many Tags ⭐⭐⭐
Difficulty: Advanced
Goal: Add tags to notes (many-to-many relationship).
Requirements:
- Create Tag model (Id, Name, Color)
- Create NoteTag join table
- Configure many-to-many relationship in DbContext
- Create migrations
- Update UI to add/remove tags
- Filter notes by tag
Hints:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// In Note model
public List<NoteTag> NoteTags { get; set; } = new();
// In Tag model
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public List<NoteTag> NoteTags { get; set; } = new();
}
// Join table
public class NoteTag
{
public int NoteId { get; set; }
public Note Note { get; set; }
public int TagId { get; set; }
public Tag Tag { get; set; }
}
// In OnModelCreating
modelBuilder.Entity<NoteTag>()
.HasKey(nt => new { nt.NoteId, nt.TagId }); // Composite key
Learning outcome: Many-to-many relationships, complex queries
Challenge 4: Category Management ⭐⭐
Difficulty: Intermediate
Goal: Add UI to create, edit, and delete categories.
Requirements:
- Create CategoriesPage and CategoryViewModel
- CRUD operations for categories
- Color picker for category color
- Prevent deleting categories with notes
- Update AppShell navigation
Learning outcome: Applying patterns to new entity types
Challenge 5: Note Sharing ⭐⭐⭐
Difficulty: Advanced
Goal: Share notes via platform share sheet.
Requirements:
- Add share button to note detail page
- Use MAUI Share API
- Format note as markdown or plain text
- Include category and importance in shared text
- Support sharing to various apps
Hints:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// In ViewModel
[RelayCommand]
private async Task ShareAsync()
{
var shareText = $@"# {Title}
Category: {Category?.Name ?? "None"}
Importance: {Importance}
{Content}";
await Share.Default.RequestAsync(new ShareTextRequest
{
Text = shareText,
Title = Title
});
}
Learning outcome: Platform APIs, MAUI essentials
Challenge 6: Offline Queue ⭐⭐⭐⭐
Difficulty: Expert
Goal: Queue changes when offline, sync when online.
Requirements:
- Detect network connectivity
- Queue CRUD operations when offline
- Store queue in local database
- Process queue when online
- Handle conflicts (note edited offline and online)
- Show sync status to user
Hints:
1
2
3
4
5
6
7
8
public class SyncQueue
{
public int Id { get; set; }
public string Operation { get; set; } // "Create", "Update", "Delete"
public int? NoteId { get; set; }
public string Data { get; set; } // JSON serialized note
public DateTime QueuedAt { get; set; }
}
Learning outcome: Offline-first architecture, conflict resolution
5.7: Verification Checklist
Use this checklist to ensure you’ve completed all tutorial objectives.
Functionality Checklist
- Create Note: Create new notes with title, content, category, importance
- Read Notes: View all notes in list with filtering
- Update Note: Edit existing notes and save changes
- Delete Note: Delete via detail page and swipe-to-delete
- Category Filtering: Filter notes by category
- Navigation: Navigate between list and detail pages
- Data Persistence: Data survives app restart
- Pull-to-Refresh: Refresh notes list
- Empty State: Shows helpful message when no notes
- Loading Indicators: Shows spinners during operations
Architecture Checklist
- MVVM Pattern: Views bind to ViewModels, no business logic in code-behind
- Repository Pattern: ViewModels use INoteRepository, not DbContext directly
- Dependency Injection: All dependencies injected, not newed up
- Entity Framework: Models map to database tables
- Migrations: Schema changes tracked and versioned
- PostgreSQL: Data stored in PostgreSQL container
- Multi-Project: Solution split into App, Database, Migrations projects
- Navigation: Shell-based navigation with routes
- Data Binding: Two-way binding between XAML and ViewModels
- ObservableCollection: List updates automatically notify UI
Code Quality Checklist
- Error Handling: Try-catch blocks around async operations
- Validation: Check required fields before saving
- Confirmation Dialogs: Ask before deleting
- Timestamps: Track CreatedAt and UpdatedAt
- Null Safety: Handle null categories gracefully
- Async/Await: All database operations async
- Naming Conventions: Clear, descriptive names
- Comments: Key sections documented
- XML Documentation: Public methods have summary comments
5.8: What You’ve Learned
Part-by-Part Recap
Part 1: Download and Explore
- Multi-project solution structure
- MVVM pattern with CommunityToolkit.Mvvm
- Entity Framework Core with PostgreSQL
- Dependency injection fundamentals
- Authentication patterns (before simplifying)
Part 2: Simplify to Note-Taking App
- Refactoring existing codebases
- Domain modeling (Note, Category)
- Database relationships (one-to-many)
- Creating and applying migrations
- Data seeding with HasData
- Building ViewModels and Views
- Shell navigation
Part 3: Add Repository Pattern
- Repository pattern for data abstraction
- Interface-based design
- Dependency injection with scoped lifetime
- Refactoring for testability
- Preparing for API integration
- Service layer responsibilities
Part 4: Advanced Migrations
- Schema evolution strategies
- Adding properties with migrations
- Migration tracking with __EFMigrationsHistory
- Team collaboration practices
- Production deployment strategies
- Complex migration scenarios
- Rollback capabilities
Part 5: Testing and Verification
- Systematic CRUD testing
- Navigation verification
- Data binding validation
- Database persistence checks
- Architecture review
- Extension challenges
Key Architectural Patterns Mastered
1. MVVM (Model-View-ViewModel)
1
2
3
4
5
6
7
View (XAML)
↕ Data Binding
ViewModel (Presentation Logic)
↕ Method Calls
Service/Repository (Business Logic)
↕ Queries
Model/Database (Data)
Benefits you’ve experienced:
- ✅ Testable ViewModels without UI
- ✅ Reusable across platforms
- ✅ Clear separation of concerns
- ✅ Two-way data binding
2. Repository Pattern
1
ViewModel → INoteRepository → NoteRepository → DbContext → Database
Benefits you’ve experienced:
- ✅ Swappable implementations (can switch to API)
- ✅ Testable with mocking
- ✅ Centralized data access logic
- ✅ ViewModels don’t know about database
3. Dependency Injection
1
2
3
4
5
MauiProgram.cs registers:
INoteRepository → NoteRepository
ViewModels receive via constructor:
public NoteViewModel(INoteRepository repository)
Benefits you’ve experienced:
- ✅ Loose coupling
- ✅ Easy to swap implementations
- ✅ Testable with mocks
- ✅ Lifetime management (Singleton, Scoped, Transient)
Technologies You Can Now Use
Frontend:
- ✅ .NET MAUI for cross-platform apps
- ✅ XAML for declarative UI
- ✅ CommunityToolkit.Mvvm for MVVM helpers
- ✅ Shell navigation for routing
Backend:
- ✅ Entity Framework Core for ORM
- ✅ PostgreSQL for relational data
- ✅ Npgsql provider for EF Core
- ✅ Migrations for schema evolution
Architecture:
- ✅ Multi-project solutions
- ✅ MVVM pattern
- ✅ Repository pattern
- ✅ Dependency Injection
Skills You’ve Developed
Development Skills:
- Reading and understanding production-quality code
- Refactoring existing codebases
- Designing domain models
- Writing migrations
- Creating repositories
- Building ViewModels
- Designing XAML layouts
- Implementing data binding
- Testing systematically
Software Engineering Practices:
- Separation of concerns
- Interface-based design
- Version control with migrations
- Team collaboration strategies
- Testing approaches
- Production deployment considerations
5.9: Next Steps and Further Learning
Immediate Next Steps
1. Experiment with the Code
- Try the extension challenges
- Break something and fix it
- Refactor with different patterns
- Add your own features
2. Study Real-World Apps
- Browse .NET MAUI samples: github.com/dotnet/maui-samples
- Analyze open-source MAUI apps
- Study architecture patterns in production apps
3. Build Your Own App
- Start small (shopping list, habit tracker)
- Apply patterns you’ve learned
- Expand gradually
- Focus on architecture, not features
Advanced Topics to Explore
1. Unit Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
// Learn to write tests like:
[Test]
public async Task CreateNote_ShouldAddToDatabase()
{
var mockRepo = new Mock<INoteRepository>();
var viewModel = new NoteViewModel(mockRepo.Object);
viewModel.Title = "Test";
viewModel.Content = "Content";
await viewModel.SaveCommand.ExecuteAsync(null);
mockRepo.Verify(r => r.CreateNoteAsync(It.IsAny<Note>()), Times.Once);
}
Resources:
- NUnit or xUnit testing frameworks
- Moq for mocking
- Test-Driven Development (TDD)
2. REST API Integration
Implement ApiNoteRepository:
1
2
3
4
5
6
7
8
9
10
public class ApiNoteRepository : INoteRepository
{
private readonly HttpClient _httpClient;
public async Task<List<Note>> GetAllNotesAsync(int? categoryId = null)
{
var response = await _httpClient.GetAsync("api/notes");
return await response.Content.ReadFromJsonAsync<List<Note>>();
}
}
Resources:
- ASP.NET Core Web API
- RESTful API design
- Authentication (JWT tokens)
- API versioning
3. Advanced EF Core
Topics to master:
- Lazy vs Eager vs Explicit loading
- Query optimization
- Computed columns
- Change tracking
- Interceptors
- Global query filters
- Owned entity types
4. MAUI Platform Features
Explore:
- Camera and photo picker
- Geolocation
- Notifications
- Background services
- Platform-specific code
- Custom renderers
5. Performance Optimization
Learn about:
- Virtual scrolling in CollectionView
- Image caching
- Database indexing
- Async best practices
- Memory profiling
- Startup time optimization
Recommended Resources
Official Documentation:
Books:
- “Enterprise Application Patterns Using .NET MAUI” by Michael Stonis and Matt Goldman
- “Entity Framework Core in Action” by Jon P. Smith
- “Clean Architecture” by Robert C. Martin
Video Tutorials:
- .NET MAUI for Beginners (Microsoft)
- James Montemagno’s MAUI tutorials on YouTube
- Nick Chapsas’s C# best practices
Community:
- r/dotnet on Reddit
- .NET MAUI Discord
- Stack Overflow [maui] tag
Summary
Congratulations! You’ve completed the MAUI + MVVM + Database tutorial series.
What You Achieved
✅ Built a complete note-taking application from scratch ✅ Mastered MVVM pattern with real production code ✅ Implemented repository pattern for data abstraction ✅ Used Entity Framework Core with PostgreSQL ✅ Created and applied database migrations ✅ Designed multi-project solution architecture ✅ Implemented dependency injection throughout ✅ Created data-bound XAML interfaces ✅ Tested systematically and verified persistence
Why This Matters
You didn’t just learn how to build an app - you learned how to build maintainable, testable, scalable applications using industry-standard patterns and practices.
These skills transfer to:
- Mobile app development (iOS, Android)
- Desktop app development (Windows, macOS)
- Web development (Blazor, ASP.NET Core)
- Enterprise application development
- Team-based professional development
Closing Thoughts
You started with a complex authentication app (StarterApp).
You refactored it into a simpler note-taking app.
You enhanced it with repository pattern.
You evolved the schema with migrations.
You tested systematically and verified quality.
Along the way, you:
- Read real production code
- Understood architectural decisions
- Made design trade-offs
- Practiced refactoring
- Thought about testing
- Considered team collaboration
- Planned for production deployment
These are the skills that separate developers who can follow tutorials from developers who can architect systems.
Keep building. Keep learning. Keep growing.
Tutorial complete! 🎉
Total series time: 6-9 hours across 5 parts Total concepts mastered: 50+ Total code written: 2,000+ lines Skills gained: Priceless 💎