diff --git a/instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md b/instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md
index c162e1e..e4e375e 100644
--- a/instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md
+++ b/instructions/dotnet-maui-9-to-dotnet-maui-10-upgrade.instructions.md
@@ -17,6 +17,10 @@ This guide helps you upgrade your .NET MAUI application from .NET 9 to .NET 10 b
- [MessagingCenter Made Internal](#messagingcenter-made-internal)
- [ListView and TableView Deprecated](#listview-and-tableview-deprecated)
4. [Deprecated APIs (P1 - Fix Soon)](#deprecated-apis-p1---fix-soon)
+ - [Animation Methods](#1-animation-methods)
+ - [DisplayAlert and DisplayActionSheet](#2-displayalert-and-displayactionsheet)
+ - [Page.IsBusy](#3-pageisbusy)
+ - [MediaPicker APIs](#4-mediapicker-apis)
5. [Recommended Changes (P2)](#recommended-changes-p2)
6. [Bulk Migration Tools](#bulk-migration-tools)
7. [Testing Your Upgrade](#testing-your-upgrade)
@@ -32,7 +36,7 @@ This guide helps you upgrade your .NET MAUI application from .NET 9 to .NET 10 b
2. **Update CommunityToolkit.Maui** to 12.3.0+ (if you use it) - REQUIRED
3. **Fix breaking changes** - MessagingCenter (P0)
4. **Migrate ListView/TableView to CollectionView** (P0 - CRITICAL)
-5. **Fix deprecated APIs** - Animation methods, DisplayAlert, IsBusy (P1)
+5. **Fix deprecated APIs** - Animation methods, DisplayAlert, IsBusy, MediaPicker (P1)
> ⚠️ **Major Breaking Changes**:
> - CommunityToolkit.Maui **must** be version 12.3.0 or later
@@ -57,7 +61,7 @@ This guide helps you upgrade your .NET MAUI application from .NET 9 to .NET 10 b
```xml
- net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0-windows10.0.19041.0
+ net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0-windows10.0.22621.0
```
@@ -76,7 +80,7 @@ This guide helps you upgrade your .NET MAUI application from .NET 9 to .NET 10 b
$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst
- $(TargetFrameworks);net10.0-windows10.0.19041.0
+ $(TargetFrameworks);net10.0-windows10.0.22621.0
```
@@ -1087,6 +1091,253 @@ public class MyViewModel : INotifyPropertyChanged
---
+### 4. MediaPicker APIs
+
+**Status:** ⚠️ **DEPRECATED** - Single-selection methods replaced with multi-selection variants.
+
+**Warning You'll See:**
+```
+warning CS0618: 'MediaPicker.PickPhotoAsync(MediaPickerOptions)' is obsolete: 'Switch to PickPhotosAsync which also allows multiple selections.'
+warning CS0618: 'MediaPicker.PickVideoAsync(MediaPickerOptions)' is obsolete: 'Switch to PickVideosAsync which also allows multiple selections.'
+```
+
+**What Changed:**
+- `PickPhotoAsync()` → `PickPhotosAsync()` (returns `List`)
+- `PickVideoAsync()` → `PickVideosAsync()` (returns `List`)
+- New `SelectionLimit` property on `MediaPickerOptions` (default: 1)
+- Old methods still work but are marked obsolete
+
+**Key Behavior:**
+- **Default behavior preserved:** `SelectionLimit = 1` (single selection)
+- Set `SelectionLimit = 0` for unlimited multi-select
+- Set `SelectionLimit > 1` for specific limits
+
+**Platform Notes:**
+- ✅ **iOS:** Selection limit enforced by native picker UI
+- ⚠️ **Android:** Not all custom pickers honor `SelectionLimit` - be aware!
+- ⚠️ **Windows:** `SelectionLimit` not supported - implement your own validation
+
+#### Migration Examples
+
+**Simple Photo Picker (maintain single-selection behavior):**
+```csharp
+// ❌ OLD (Deprecated)
+var photo = await MediaPicker.PickPhotoAsync(new MediaPickerOptions
+{
+ Title = "Pick a photo"
+});
+
+if (photo != null)
+{
+ var stream = await photo.OpenReadAsync();
+ MyImage.Source = ImageSource.FromStream(() => stream);
+}
+
+// ✅ NEW (maintains same behavior - picks only 1 photo)
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ Title = "Pick a photo",
+ SelectionLimit = 1 // Explicit: only 1 photo
+});
+
+var photo = photos.FirstOrDefault();
+if (photo != null)
+{
+ var stream = await photo.OpenReadAsync();
+ MyImage.Source = ImageSource.FromStream(() => stream);
+}
+```
+
+**Simple Video Picker (maintain single-selection behavior):**
+```csharp
+// ❌ OLD (Deprecated)
+var video = await MediaPicker.PickVideoAsync(new MediaPickerOptions
+{
+ Title = "Pick a video"
+});
+
+if (video != null)
+{
+ VideoPlayer.Source = video.FullPath;
+}
+
+// ✅ NEW (maintains same behavior - picks only 1 video)
+var videos = await MediaPicker.PickVideosAsync(new MediaPickerOptions
+{
+ Title = "Pick a video",
+ SelectionLimit = 1 // Explicit: only 1 video
+});
+
+var video = videos.FirstOrDefault();
+if (video != null)
+{
+ VideoPlayer.Source = video.FullPath;
+}
+```
+
+**Photo Picker without Options (uses defaults):**
+```csharp
+// ❌ OLD (Deprecated)
+var photo = await MediaPicker.PickPhotoAsync();
+
+// ✅ NEW (default SelectionLimit = 1, so same behavior)
+var photos = await MediaPicker.PickPhotosAsync();
+var photo = photos.FirstOrDefault();
+```
+
+**Multi-Photo Selection (new capability):**
+```csharp
+// ✅ NEW: Pick up to 5 photos
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ Title = "Pick up to 5 photos",
+ SelectionLimit = 5
+});
+
+foreach (var photo in photos)
+{
+ var stream = await photo.OpenReadAsync();
+ // Process each photo
+}
+
+// ✅ NEW: Unlimited selection
+var allPhotos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ Title = "Pick photos",
+ SelectionLimit = 0 // No limit
+});
+```
+
+**Multi-Video Selection (new capability):**
+```csharp
+// ✅ NEW: Pick up to 3 videos
+var videos = await MediaPicker.PickVideosAsync(new MediaPickerOptions
+{
+ Title = "Pick up to 3 videos",
+ SelectionLimit = 3
+});
+
+foreach (var video in videos)
+{
+ // Process each video
+ Console.WriteLine($"Selected: {video.FileName}");
+}
+```
+
+**Handling Empty Results:**
+```csharp
+// NEW: Returns empty list if user cancels (not null)
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ SelectionLimit = 1
+});
+
+// ✅ Check for empty list
+if (photos.Count == 0)
+{
+ await DisplayAlertAsync("Cancelled", "No photo selected", "OK");
+ return;
+}
+
+var photo = photos.First();
+// Process photo...
+```
+
+**With Try-Catch (same as before):**
+```csharp
+try
+{
+ var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+ {
+ Title = "Pick a photo",
+ SelectionLimit = 1
+ });
+
+ if (photos.Count > 0)
+ {
+ await ProcessPhotoAsync(photos.First());
+ }
+}
+catch (PermissionException)
+{
+ await DisplayAlertAsync("Permission Denied", "Camera access required", "OK");
+}
+catch (Exception ex)
+{
+ await DisplayAlertAsync("Error", $"Failed to pick photo: {ex.Message}", "OK");
+}
+```
+
+#### Migration Checklist
+
+When migrating to the new MediaPicker APIs:
+
+- [ ] Replace `PickPhotoAsync()` with `PickPhotosAsync()`
+- [ ] Replace `PickVideoAsync()` with `PickVideosAsync()`
+- [ ] Set `SelectionLimit = 1` to maintain single-selection behavior
+- [ ] Change `FileResult?` to `List` (or use `.FirstOrDefault()`)
+- [ ] Update null checks to empty list checks (`photos.Count == 0`)
+- [ ] Test on Android - ensure custom pickers respect limit (or add validation)
+- [ ] Test on Windows - add your own limit validation if needed
+- [ ] Consider if multi-select would improve your UX (optional)
+
+#### Platform-Specific Validation (Windows & Android)
+
+```csharp
+// ✅ Recommended: Validate selection limit on platforms that don't enforce it
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ Title = "Pick up to 5 photos",
+ SelectionLimit = 5
+});
+
+// On Windows and some Android pickers, the limit might not be enforced
+if (photos.Count > 5)
+{
+ await DisplayAlertAsync(
+ "Too Many Photos",
+ $"Please select up to 5 photos. You selected {photos.Count}.",
+ "OK"
+ );
+ return;
+}
+
+// Continue processing...
+```
+
+#### Capture Methods (unchanged)
+
+**Note:** Capture methods (`CapturePhotoAsync`, `CaptureVideoAsync`) are **NOT** deprecated and remain unchanged:
+
+```csharp
+// ✅ These still work as-is (no changes needed)
+var photo = await MediaPicker.CapturePhotoAsync();
+var video = await MediaPicker.CaptureVideoAsync();
+```
+
+#### Quick Migration Pattern
+
+**For all existing single-selection code, use this pattern:**
+
+```csharp
+// ❌ OLD
+var photo = await MediaPicker.PickPhotoAsync(options);
+if (photo != null)
+{
+ // Process photo
+}
+
+// ✅ NEW (drop-in replacement)
+var photos = await MediaPicker.PickPhotosAsync(options ?? new MediaPickerOptions { SelectionLimit = 1 });
+var photo = photos.FirstOrDefault();
+if (photo != null)
+{
+ // Process photo (same code as before)
+}
+```
+
+---
+
## Recommended Changes (P2)
These changes are recommended but not required immediately. Consider migrating during your next refactoring cycle.
@@ -1203,6 +1454,31 @@ Find: DisplayActionSheet\(
Replace: DisplayActionSheetAsync(
```
+#### MediaPicker Methods
+
+**⚠️ Note:** MediaPicker migration requires manual code changes due to return type changes (`FileResult?` → `List`). Use these searches to find instances:
+
+```bash
+# Find PickPhotoAsync usages
+grep -rn "PickPhotoAsync" --include="*.cs" .
+
+# Find PickVideoAsync usages
+grep -rn "PickVideoAsync" --include="*.cs" .
+```
+
+**Manual Migration Pattern:**
+```csharp
+// Find: await MediaPicker.PickPhotoAsync(
+// Replace with:
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions { SelectionLimit = 1 });
+var photo = photos.FirstOrDefault();
+
+// Find: await MediaPicker.PickVideoAsync(
+// Replace with:
+var videos = await MediaPicker.PickVideosAsync(new MediaPickerOptions { SelectionLimit = 1 });
+var video = videos.FirstOrDefault();
+```
+
#### ListView/TableView Detection (Manual Migration Required)
**⚠️ Note:** ListView/TableView migration CANNOT be automated. Use these searches to find instances:
@@ -1416,6 +1692,54 @@ dotnet workload update
---
+### Warning: MediaPicker methods are obsolete
+
+**Cause:** Using deprecated `PickPhotoAsync` or `PickVideoAsync` methods.
+
+**Solution:** Migrate to `PickPhotosAsync` or `PickVideosAsync`:
+
+```csharp
+// ❌ OLD
+var photo = await MediaPicker.PickPhotoAsync(options);
+
+// ✅ NEW (maintain single-selection)
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ Title = options?.Title,
+ SelectionLimit = 1
+});
+var photo = photos.FirstOrDefault();
+```
+
+**Key Changes:**
+- Return type changes from `FileResult?` to `List`
+- Use `.FirstOrDefault()` to get single result
+- Set `SelectionLimit = 1` to maintain old behavior
+- Check `photos.Count == 0` instead of `photo == null`
+
+---
+
+### MediaPicker returns more items than SelectionLimit
+
+**Cause:** Windows and some Android custom pickers don't enforce `SelectionLimit`.
+
+**Solution:** Add manual validation:
+
+```csharp
+var photos = await MediaPicker.PickPhotosAsync(new MediaPickerOptions
+{
+ SelectionLimit = 5
+});
+
+if (photos.Count > 5)
+{
+ await DisplayAlertAsync("Error", "Too many photos selected", "OK");
+ return;
+}
+```
+
+---
+
### Animation doesn't complete after migration
**Cause:** Forgetting `await` keyword.
@@ -1556,6 +1880,8 @@ warning CS0618: 'ListView' is obsolete: 'With the deprecation of ListView, this
- [ ] Update `DisplayAlert` → `DisplayAlertAsync`
- [ ] Update `DisplayActionSheet` → `DisplayActionSheetAsync`
- [ ] Replace `Page.IsBusy` with `ActivityIndicator`
+- [ ] Replace `PickPhotoAsync` → `PickPhotosAsync` (with `SelectionLimit = 1`)
+- [ ] Replace `PickVideoAsync` → `PickVideosAsync` (with `SelectionLimit = 1`)
**Nice to Have (P2):**
- [ ] Migrate `Application.MainPage` to `CreateWindow`