Part 5: Testing and Verification

Table of contents
  1. Part 5: Testing and Verification
    1. Learning Objectives
    2. 5.1: Complete CRUD Workflow Testing
      1. Setup: Clean Environment
      2. Test 1: Create Note
      3. Test 2: Read Notes List
      4. Test 3: Filter by Category
      5. Test 4: Read Single Note (Detail View)
      6. Test 5: Update Existing Note
      7. Test 6: Delete Note (From Detail Page)
      8. Test 7: Delete Note (Swipe-to-Delete)
    3. 5.2: Navigation Testing
      1. Test 8: Navigation Flow
    4. 5.3: Data Binding Verification
      1. Test 9: Two-Way Data Binding
      2. Test 10: ObservableCollection Updates
    5. 5.4: Database Persistence
      1. Test 11: Application Restart Persistence
      2. Test 12: Direct Database Verification
    6. 5.5: Architecture Review
      1. Complete Architecture Diagram
      2. Data Flow: Creating a Note
    7. 5.6: Extension Challenges
      1. Challenge 1: Add Search Functionality ⭐
      2. Challenge 2: Soft Delete ⭐⭐
      3. Challenge 3: Many-to-Many Tags ⭐⭐⭐
      4. Challenge 4: Category Management ⭐⭐
      5. Challenge 5: Note Sharing ⭐⭐⭐
      6. Challenge 6: Offline Queue ⭐⭐⭐⭐
    8. 5.7: Verification Checklist
      1. Functionality Checklist
      2. Architecture Checklist
      3. Code Quality Checklist
    9. 5.8: What You’ve Learned
      1. Part-by-Part Recap
      2. Key Architectural Patterns Mastered
      3. Technologies You Can Now Use
      4. Skills You’ve Developed
    10. 5.9: Next Steps and Further Learning
      1. Immediate Next Steps
      2. Advanced Topics to Explore
      3. Recommended Resources
    11. Summary
      1. What You Achieved
      2. Why This Matters
      3. Closing Thoughts

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:

  1. Launch app
    • App should open to NotesPage
    • Title shows “My Notes”
    • If empty, shows empty state: “No notes yet”
  2. Tap “+” button (toolbar)
    • Navigates to NotePage
    • Title shows “New Note”
    • All fields empty except importance (defaults to Normal)
    • Category picker populated with categories
  3. Fill in note details:
    • Title: “Team Meeting Notes”
    • Category: Select “Work”
    • Importance: Select “High”
    • Content: “Discussed Q1 goals and timeline”
  4. Tap “Create” button
    • No error messages appear
    • Navigates back to NotesPage
    • Loading spinner appears briefly
  5. 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 CreateNoteAsync in repository
  • Ensure SaveChangesAsync is called

Test 2: Read Notes List

Objective: Verify list displays all notes with correct data.

Steps:

  1. Create multiple notes with different properties:
    • “Grocery Shopping” - Shopping category, Normal importance
    • “Finish Assignment” - Study category, High importance
    • “Workout Plan” - Personal category, Low importance
  2. 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
  3. 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 GetAllNotesAsync includes .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:

  1. Verify “All Categories” selected by default
    • All 4 notes visible
  2. Select “Work” category:
    • List updates automatically
    • Only “Team Meeting Notes” visible
    • Other notes hidden
  3. Select “Study” category:
    • Only “Finish Assignment” visible
  4. Select “Shopping” category:
    • Only “Grocery Shopping” visible
  5. Select “Personal” category:
    • Only “Workout Plan” visible
  6. Select “All Categories” again:
    • All 4 notes visible again

Expected Result: ✅ Filtering works correctly for all categories

If test fails:

  • Check OnSelectedCategoryIdChanged partial method
  • Verify repository filtering logic
  • Check if LoadNotesAsync is called on filter change

Test 4: Read Single Note (Detail View)

Objective: Verify editing page loads note correctly.

Steps:

  1. Tap “Team Meeting Notes” in list
    • Navigates to NotePage
    • Title shows “Edit Note”
    • URL includes ?id=1 (or appropriate ID)
  2. Verify all fields populated:
    • Title: “Team Meeting Notes” ✓
    • Category: “Work” selected ✓
    • Importance: “High” selected ✓
    • Content: Full text visible ✓
    • Delete button: Visible at bottom ✓
  3. 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 GetNoteByIdAsync in repository
  • Verify InitializeAsync in NoteViewModel
  • Check query parameter parsing in NotePage.xaml.cs

Test 5: Update Existing Note

Objective: Verify note updates persist correctly.

Steps:

  1. Tap “Grocery Shopping” in list
    • Edit page opens
  2. Modify all fields:
    • Change title to “Weekly Grocery Shopping”
    • Change category to “Personal”
    • Change importance to “High”
    • Add to content: “Don’t forget milk!”
  3. Tap “Update” button
    • No error messages
    • Navigates back to list
    • Loading spinner appears briefly
  4. 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 ✓
  5. 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 UpdateNoteAsync in repository
  • Verify UpdatedAt timestamp set
  • Ensure SaveChangesAsync called after modifications

Test 6: Delete Note (From Detail Page)

Objective: Verify deletion from detail page works.

Steps:

  1. Tap “Workout Plan” in list
    • Edit page opens
  2. Scroll to bottom
    • Red “Delete Note” button visible
  3. Tap “Delete Note”
    • Confirmation dialog appears
    • Message: “Are you sure you want to delete this note?”
    • Two buttons: “Delete” and “Cancel”
  4. Tap “Cancel”
    • Dialog closes
    • Note NOT deleted
    • Still on edit page
  5. Tap “Delete Note” again
    • Dialog appears again
  6. Tap “Delete”
    • Dialog closes
    • Navigates back to NotesPage
    • Note removed from list
  7. Verify deletion persisted:
    • Close app completely
    • Restart app
    • “Workout Plan” still gone

Expected Result: ✅ Note deleted and removal persists

If test fails:

  • Check DeleteNoteAsync in repository
  • Verify Remove() and SaveChangesAsync called
  • Check navigation after deletion

Test 7: Delete Note (Swipe-to-Delete)

Objective: Verify swipe gesture deletion works.

Steps:

  1. In NotesPage, find “Finish Assignment”

  2. Swipe left on the note
    • Red delete button appears on right side
  3. Tap red “Delete” button
    • Confirmation dialog appears
  4. Tap “Cancel”
    • Note NOT deleted
    • Swipe area closes
  5. Swipe left again

  6. Tap “Delete” button

  7. Tap “Delete” in confirmation
    • Note immediately removed from list
    • No navigation (stays on NotesPage)
  8. 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:

  1. Test flyout menu:
    • Tap hamburger menu (≡) or swipe from left
    • Flyout menu opens
    • See “Notes” and “About” items
  2. Navigate to About page:
    • Tap “About” in flyout
    • AboutPage loads
    • Shows app information
  3. Navigate back to Notes:
    • Tap “Notes” in flyout
    • NotesPage loads
    • All notes still visible
  4. Test create navigation:
    • Tap “+” button
    • NotePage loads for creation
    • Tap “Cancel”
    • Back to NotesPage (no note created)
  5. Test edit navigation:
    • Tap a note
    • NotePage loads for editing
    • Tap device back button (Android) or gesture (iOS)
    • Back to NotesPage (no changes saved)
  6. 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

  1. Open any note for editing

  2. Modify title in Entry field
    • Type: “Updated Title”
    • ViewModel Title property updates automatically
    • No explicit code needed
  3. Clear title completely
    • Entry shows placeholder “Enter note title”
    • Tap “Update”
    • Error message appears: “Title is required”
    • ViewModel validation works ✓

Test B: Command Execution

  1. 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

Test C: Picker Binding

  1. Open note for editing
  2. Change category picker:
    • Select different category
    • ViewModel SelectedCategoryId updates
    • 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 BindingContext set correctly in code-behind

Test 10: ObservableCollection Updates

Objective: Verify list updates automatically when data changes.

Steps:

  1. Have NotesPage open with 2+ notes visible

  2. Create new note:
    • Tap “+”
    • Fill form
    • Tap “Create”
    • Observe: New note appears in list WITHOUT manual refresh
    • ObservableCollection automatically notified UI ✓
  3. Delete note via swipe:
    • Swipe and delete
    • Observe: Note disappears immediately
    • No delay, no manual refresh needed ✓
  4. 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:

  1. Create unique test note:
    • Title: “Persistence Test - [Current Time]”
    • Add specific content you’ll recognize
    • Save note
  2. 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
  3. Wait 10 seconds (ensure process terminated)

  4. Restart application:
    • Launch app fresh
    • NotesPage loads
  5. Verify test note still exists:
    • Find “Persistence Test” note
    • Tap to view details
    • All data intact (title, content, category, importance)
  6. Modify test note:
    • Change something
    • Save
  7. 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:

  1. Create note with specific data in app:
    • Title: “Database Verification Test”
    • Category: “Work”
    • Importance: “High”
    • Content: “This is a verification test”
  2. 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
  1. Verify field mappings:
    • importance = 2 → High (enum value) ✓
    • category_id = 2 → Work category ✓
    • Timestamps in UTC ✓
  2. 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
  1. 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 notes to 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 IsDeleted boolean 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

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

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:


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 💎


This site uses Just the Docs, a documentation theme for Jekyll.