Skip to content

Commit 7b45118

Browse files
authored
December 22nd, Candidate (dotnet#33185)
## CollectionView - Fixed the NRE in CarouselViewController on iOS 15.5 & 16.4 by @Ahamed-Ali in dotnet#30838 <details> <summary>🔧 Fixes</summary> - [NRE in CarouselViewController on iOS 15.5 & 16.4](dotnet#28557) </details> - [iOS, macOS] Fixed CollectionView group header size changes with ItemSizingStrategy by @NanthiniMahalingam in dotnet#33161 <details> <summary>🔧 Fixes</summary> - [[NET 10] I6_Grouping - Grouping_with_variable_sized_items changing the 'ItemSizingStrategy' also changes the header size.](dotnet#33130) </details> ## Flyout - Add unit tests for TabBar and FlyoutItem navigation ApplyQueryAttributes (dotnet#25663) by @StephaneDelcroix in dotnet#33006 ## Flyoutpage - Fixed the FlyoutPage.Flyout Disappearing When Maximizing the Window on Mac Platform by @NanthiniMahalingam in dotnet#26701 <details> <summary>🔧 Fixes</summary> - [FlyoutPage.Flyout - navigation corrupted when running om mac , on window ok](dotnet#22719) </details> ## Mediapicker - [Windows] Fix for PickPhotosAsync throws exception if image is modified by @HarishwaranVijayakumar in dotnet#32952 <details> <summary>🔧 Fixes</summary> - [PickPhotosAsync throws exception if image is modified.](dotnet#32408) </details> ## Navigation - Fix for TabBar Navigation does not invoke its IQueryAttributable.ApplyQueryAttributes(query) by @SuthiYuvaraj in dotnet#25663 <details> <summary>🔧 Fixes</summary> - [Tabs defined in AppShell.xaml does not invoke its view model's IQueryAttributable.ApplyQueryAttributes(query) implementaion](dotnet#13537) - [`ShellContent` routes do not call `ApplyQueryAttributes`](dotnet#28453) </details> ## ScrollView - Fix ScrollToPosition.Center behavior in ScrollView on iOS and MacCatalyst by @devanathan-vaithiyanathan in dotnet#26825 <details> <summary>🔧 Fixes</summary> - [ScrollToPosition.Center Centers the First Item too in iOS and Catalyst](dotnet#26760) - [On iOS - ScrollView.ScrollToAsync Element, ScrollToPosition.MakeVisible shifts view to the right, instead of just scrolling vertically](dotnet#28965) </details> ## Searchbar - [iOS, Mac, Windows] Fixed CharacterSpacing for SearchBar text and placeholder text by @Dhivya-SF4094 in dotnet#30407 <details> <summary>🔧 Fixes</summary> - [[iOS, Mac, Windows] SearchBar CharacterSpacing property is not working as expected](dotnet#30366) </details> ## Shell - Update logic for large title display mode on iOS - shell by @kubaflo in dotnet#33039 ## TitleView - [iOS] Fixed memory leak with PopToRootAsync when using TitleView by @Vignesh-SF3580 in dotnet#28547 <details> <summary>🔧 Fixes</summary> - [NavigationPage.TitleView causes memory leak with PopToRootAsync](dotnet#28201) </details> ## Xaml - [C] Fix binding to interface-inherited properties like IReadOnlyList<T>.Count by @StephaneDelcroix in dotnet#32912 <details> <summary>🔧 Fixes</summary> - [Compiled Binding to Array.Count provides no result](dotnet#13872) </details> - Fix dotnet#31939: CommandParameter TemplateBinding lost during reparenting by @StephaneDelcroix in dotnet#32961 <details> <summary>🔧 Fixes</summary> - [CommandParameter TemplateBinding Lost During ControlTemplate Reparenting](dotnet#31939) </details> <details> <summary>🧪 Testing (4)</summary> - [Testing] Fixed Test case failure in PR 33185 - [12/22/2025] Candidate by @TamilarasanSF4853 in dotnet#33257 - [Testing] Re-saved ShouldFlyoutBeVisibleAfterMaximizingWindow test case images in PR 33185 - [12/22/2025] Candidate by @TamilarasanSF4853 in dotnet#33271 - [Testing] Fixed Test case failure in PR 33185 - [12/22/2025] Candidate - 2 by @TamilarasanSF4853 in dotnet#33299 - [Testing] Fixed Test case failure in PR 33185 - [12/22/2025] Candidate - 3 by @TamilarasanSF4853 in dotnet#33311 </details> <details> <summary>📦 Other (2)</summary> - [XSG][BindingSourceGen] Add support for RelayCommand to compiled bindings by @simonrozsival via @Copilot in dotnet#32954 <details> <summary>🔧 Fixes</summary> - [Issue dotnet#25818](dotnet#25818) </details> - Revert "Update logic for large title display mode on iOS - shell (dotnet#33039)" in dotnet@cff7f35 </details> **Full Changelog**: dotnet/maui@main...inflight/candidate
2 parents dec14ce + 5f19e7e commit 7b45118

67 files changed

Lines changed: 1737 additions & 123 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,20 @@ private static Result<ITypeSymbol> GetLambdaReturnType(LambdaExpressionSyntax la
210210
var lambdaResultType = semanticModel.GetTypeInfo(lambdaBody, t).Type;
211211
if (lambdaResultType == null || lambdaResultType is IErrorTypeSymbol)
212212
{
213+
// Try to infer the type from known patterns (e.g., RelayCommand properties)
214+
if (lambdaBody is MemberAccessExpressionSyntax memberAccess)
215+
{
216+
var memberName = memberAccess.Name.Identifier.Text;
217+
var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type;
218+
219+
if (expressionType != null &&
220+
expressionType.TryGetRelayCommandPropertyType(memberName, semanticModel.Compilation, out var commandType) &&
221+
commandType != null)
222+
{
223+
return Result<ITypeSymbol>.Success(commandType);
224+
}
225+
}
226+
213227
return Result<ITypeSymbol>.Failure(DiagnosticsFactory.LambdaResultCannotBeResolved(lambdaBody.GetLocation()));
214228
}
215229

src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Linq;
12
using Microsoft.CodeAnalysis;
23

34
namespace Microsoft.Maui.Controls.BindingSourceGen;
@@ -43,4 +44,74 @@ private static string GetGlobalName(this ITypeSymbol typeSymbol, bool isNullable
4344

4445
return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4546
}
47+
48+
/// <summary>
49+
/// Checks if a property name could be generated by CommunityToolkit.Mvvm's [RelayCommand] attribute,
50+
/// and returns the inferred command type if found.
51+
/// </summary>
52+
/// <param name="symbol">The type to search</param>
53+
/// <param name="propertyName">The name of the property to find (should end with "Command")</param>
54+
/// <param name="compilation">The compilation (can be null)</param>
55+
/// <param name="commandType">The inferred ICommand type if a RelayCommand method is found</param>
56+
/// <returns>True if a RelayCommand method was found that would generate this property</returns>
57+
public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, string propertyName, Compilation? compilation, out ITypeSymbol? commandType)
58+
{
59+
commandType = null;
60+
61+
if (compilation == null)
62+
return false;
63+
64+
// Check if the property name ends with "Command"
65+
if (!propertyName.EndsWith("Command", System.StringComparison.Ordinal))
66+
return false;
67+
68+
// Extract the method name (property name without "Command" suffix)
69+
var methodName = propertyName.Substring(0, propertyName.Length - "Command".Length);
70+
71+
// Look for a method with the base name - search in the type and base types
72+
var methods = GetAllMethods(symbol, methodName);
73+
74+
foreach (var method in methods)
75+
{
76+
// Check if the method has the RelayCommand attribute
77+
var hasRelayCommand = method.GetAttributes().Any(attr =>
78+
attr.AttributeClass?.Name == "RelayCommandAttribute" ||
79+
attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.Input.RelayCommandAttribute");
80+
81+
if (hasRelayCommand)
82+
{
83+
// Try to find the ICommand interface type
84+
var icommandType = compilation.GetTypeByMetadataName("System.Windows.Input.ICommand");
85+
if (icommandType != null)
86+
{
87+
commandType = icommandType;
88+
return true;
89+
}
90+
}
91+
}
92+
93+
return false;
94+
}
95+
96+
private static System.Collections.Generic.IEnumerable<IMethodSymbol> GetAllMethods(ITypeSymbol symbol, string name)
97+
{
98+
// Search in current type
99+
foreach (var member in symbol.GetMembers(name))
100+
{
101+
if (member is IMethodSymbol method)
102+
yield return method;
103+
}
104+
105+
// Search in base types
106+
var baseType = symbol.BaseType;
107+
while (baseType != null)
108+
{
109+
foreach (var member in baseType.GetMembers(name))
110+
{
111+
if (member is IMethodSymbol method)
112+
yield return method;
113+
}
114+
baseType = baseType.BaseType;
115+
}
116+
}
46117
}

src/Controls/src/BindingSourceGen/PathParser.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,17 @@ private Result<List<IPathPart>> HandleMemberAccessExpression(MemberAccessExpress
4444
var typeInfo = _context.SemanticModel.GetTypeInfo(memberAccess).Type;
4545
var symbol = _context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
4646

47+
// Handle known special cases when symbol or type are not resolved at compile time
4748
if (symbol == null || typeInfo == null)
4849
{
50+
// Try to infer from known patterns (e.g., RelayCommand properties)
51+
var expressionType = _context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
52+
if (expressionType != null && TryHandleSpecialCases(member, expressionType, out var specialCasePart) && specialCasePart != null)
53+
{
54+
result.Value.Add(specialCasePart);
55+
return Result<List<IPathPart>>.Success(result.Value);
56+
}
57+
4958
return Result<List<IPathPart>>.Failure(DiagnosticsFactory.UnableToResolvePath(memberAccess.GetLocation()));
5059
}
5160

@@ -73,6 +82,32 @@ private Result<List<IPathPart>> HandleMemberAccessExpression(MemberAccessExpress
7382
return Result<List<IPathPart>>.Success(result.Value);
7483
}
7584

85+
private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType, out IPathPart? pathPart)
86+
{
87+
pathPart = null;
88+
89+
// Check for RelayCommand-generated properties
90+
if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType)
91+
&& commandType != null)
92+
{
93+
var memberType = commandType.CreateTypeDescription(_enabledNullable);
94+
var containingType = expressionType.CreateTypeDescription(_enabledNullable);
95+
96+
pathPart = new MemberAccess(
97+
MemberName: memberName,
98+
IsValueType: !commandType.IsReferenceType,
99+
ContainingType: containingType,
100+
MemberType: memberType,
101+
Kind: AccessorKind.Property,
102+
IsGetterInaccessible: false, // Assume generated property is accessible
103+
IsSetterInaccessible: true); // Commands are typically read-only
104+
105+
return true;
106+
}
107+
108+
return false;
109+
}
110+
76111
private Result<List<IPathPart>> HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess)
77112
{
78113
var result = ParsePath(elementAccess.Expression);

src/Controls/src/Core/BindingExpression.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,28 @@ PropertyInfo GetIndexer(TypeInfo sourceType, string indexerName, string content)
318318
return null;
319319
}
320320

321+
PropertyInfo GetProperty(TypeInfo sourceType, string propertyName)
322+
{
323+
// First, check the type and its base classes
324+
TypeInfo type = sourceType;
325+
do
326+
{
327+
var property = type.GetDeclaredProperty(propertyName);
328+
if (property != null)
329+
return property;
330+
} while ((type = type.BaseType?.GetTypeInfo()) != null);
331+
332+
// If not found, check implemented interfaces (for interface-inherited properties like IReadOnlyList<T>.Count)
333+
foreach (var iface in sourceType.ImplementedInterfaces)
334+
{
335+
var property = GetProperty(iface.GetTypeInfo(), propertyName);
336+
if (property != null)
337+
return property;
338+
}
339+
340+
return null;
341+
}
342+
321343

322344
void SetupPart(TypeInfo sourceType, BindingExpressionPart part)
323345
{
@@ -382,11 +404,7 @@ void SetupPart(TypeInfo sourceType, BindingExpressionPart part)
382404
}
383405
else
384406
{
385-
TypeInfo type = sourceType;
386-
do
387-
{
388-
property = type.GetDeclaredProperty(part.Content);
389-
} while (property == null && (type = type.BaseType?.GetTypeInfo()) != null);
407+
property = GetProperty(sourceType, part.Content);
390408
}
391409
if (property != null)
392410
{

src/Controls/src/Core/Compatibility/Handlers/FlyoutPage/iOS/PhoneFlyoutPageRenderer.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,17 @@ bool shouldReceive(UIGestureRecognizer g, UITouch t)
197197
public override void ViewWillTransitionToSize(CoreGraphics.CGSize toSize, IUIViewControllerTransitionCoordinator coordinator)
198198
{
199199
base.ViewWillTransitionToSize(toSize, coordinator);
200-
201-
if (FlyoutOverlapsDetailsInPopoverMode)
200+
if (!OperatingSystem.IsMacCatalyst())
202201
{
203-
if (FlyoutPageController.ShouldShowSplitMode)
204-
UpdatePresented(true);
205-
else
206-
UpdatePresented(false);
207-
}
208-
else
209-
{
210-
if (!FlyoutPageController.ShouldShowSplitMode && _presented)
202+
bool shouldShowSplitMode = FlyoutPageController.ShouldShowSplitMode;
203+
if (FlyoutOverlapsDetailsInPopoverMode)
204+
{
205+
UpdatePresented(shouldShowSplitMode);
206+
}
207+
else if (!shouldShowSplitMode && _presented)
208+
{
211209
UpdatePresented(false);
210+
}
212211
}
213212

214213
UpdateLeftBarButton();

src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,11 +311,26 @@ protected virtual async Task<bool> OnPopToRoot(Page page, bool animated)
311311

312312
var task = GetAppearedOrDisappearedTask(page);
313313

314-
PopToRootViewController(animated);
314+
var poppedControllers = PopToRootViewController(animated);
315315

316316
_ignorePopCall = false;
317317
var success = !await task;
318318

319+
if (poppedControllers is not null)
320+
{
321+
foreach (var poppedController in poppedControllers)
322+
{
323+
if (poppedController is ParentingViewController parentingViewController)
324+
{
325+
parentingViewController.Disconnect(false);
326+
}
327+
else
328+
{
329+
poppedController?.Dispose();
330+
}
331+
}
332+
}
333+
319334
UpdateToolBarVisible();
320335
return success;
321336
}

src/Controls/src/Core/Handlers/Items/iOS/CarouselViewController.cs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,45 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS
6161
{
6262
UICollectionViewCell cell;
6363

64-
if (ItemsView?.Loop == true && _carouselViewLoopManager != null)
65-
{
66-
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId(indexPath));
67-
cell = cellAndCorrectedIndex.cell;
68-
var correctedIndexPath = NSIndexPath.FromRowSection(cellAndCorrectedIndex.correctedIndex, 0);
69-
70-
if (cell is DefaultCell defaultCell)
64+
if (ItemsView?.Loop == true)
65+
{
66+
// In iOS 15 and 16, when the ItemsSource of the CarouselView is updated from another page
67+
// via the ViewModel (or similar), GetCell is called immediately—while _carouselViewLoopManager
68+
// is still null. As a result, an invalid index is passed to base.GetCell, leading to an ArgumentNullException.
69+
//
70+
// However, in iOS 17 and 18, GetCell is only called after navigating back to the page containing
71+
// the CarouselView. By that time, CarouselViewLoopManager has been properly initialized during
72+
// the window attachment, so the issue does not occur.
73+
//
74+
// This fix ensures proper handling across all iOS versions by initializing the loop manager
75+
// when needed and providing a fallback implementation, making navigation scenarios work consistently.
76+
if (_carouselViewLoopManager is null)
7177
{
72-
UpdateDefaultCell(defaultCell, correctedIndexPath);
78+
InitializeCarouselViewLoopManager();
7379
}
7480

75-
if (cell is TemplatedCell templatedCell)
81+
if (_carouselViewLoopManager is not null)
7682
{
77-
UpdateTemplatedCell(templatedCell, correctedIndexPath);
83+
var cellAndCorrectedIndex = _carouselViewLoopManager.GetCellAndCorrectIndex(collectionView, indexPath, DetermineCellReuseId(indexPath));
84+
cell = cellAndCorrectedIndex.cell;
85+
var correctedIndexPath = NSIndexPath.FromRowSection(cellAndCorrectedIndex.correctedIndex, 0);
86+
87+
if (cell is DefaultCell defaultCell)
88+
{
89+
UpdateDefaultCell(defaultCell, correctedIndexPath);
90+
}
91+
92+
if (cell is TemplatedCell templatedCell)
93+
{
94+
UpdateTemplatedCell(templatedCell, correctedIndexPath);
95+
}
96+
}
97+
else
98+
{
99+
// Fallback case: If _carouselViewLoopManager is still null after attempted initialization,
100+
// we bypass loop-specific behavior and use base implementation directly.
101+
102+
cell = base.GetCell(collectionView, indexPath);
78103
}
79104
}
80105
else

src/Controls/src/Core/Handlers/Items2/iOS/GroupableItemsViewController2.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ void UpdateTemplatedSupplementaryView(TemplatedCell2 cell, NSString elementKind,
127127

128128
var bindingContext = ItemsSource.Group(indexPath);
129129

130+
// Mark this templated cell as a supplementary view (header/footer)
131+
cell.isSupplementaryView = true;
130132
cell.isHeaderOrFooterChanged = true;
131133
cell.Bind(template, bindingContext, ItemsView);
132134
cell.isHeaderOrFooterChanged = false;

src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS
119119
{
120120
TemplatedCell2.ScrollDirection = ScrollDirection;
121121

122+
// Ensure this cell is treated as a regular item cell (not a supplementary view)
123+
TemplatedCell2.isSupplementaryView = false;
122124
TemplatedCell2.Bind(ItemsView.ItemTemplate, ItemsSource[indexpathAdjusted], ItemsView);
123125
}
124126
else if (cell is DefaultCell2 DefaultCell2)

src/Controls/src/Core/Handlers/Items2/iOS/StructuredItemsViewController2.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ void UpdateTemplatedSupplementaryView(TemplatedCell2 cell, NSString elementKind)
130130
{
131131
bool isHeader = elementKind == UICollectionElementKindSectionKey.Header;
132132
cell.isHeaderOrFooterChanged = true;
133+
cell.isSupplementaryView = true;
133134

134135
if (isHeader)
135136
{

0 commit comments

Comments
 (0)