Skip to content

Commit 5252256

Browse files
authored
Merge pull request #22964 from unoplatform/dev/xygu/20260403/radiobuttons-unload-selection
fix(RadioButtons): selection reset on visual-tree unload
2 parents 19d5a18 + d87c2f6 commit 5252256

2 files changed

Lines changed: 209 additions & 3 deletions

File tree

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_RadioButton.cs

Lines changed: 200 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
using System;
2+
using System.Collections.ObjectModel;
3+
using System.ComponentModel;
4+
using System.Diagnostics;
25
using System.Threading.Tasks;
36
using System.Windows.Input;
4-
using Microsoft.UI.Xaml.Tests.Common;
5-
using Microsoft.VisualStudio.TestTools.UnitTesting;
67
using Microsoft.UI.Xaml;
78
using Microsoft.UI.Xaml.Automation.Peers;
89
using Microsoft.UI.Xaml.Controls;
10+
using Microsoft.UI.Xaml.Controls.Primitives;
11+
using Microsoft.UI.Xaml.Data;
12+
using Microsoft.UI.Xaml.Tests.Common;
13+
using Microsoft.VisualStudio.TestTools.UnitTesting;
14+
using Uno.UI.RuntimeTests.Helpers;
15+
using Windows.Foundation;
916
using static Private.Infrastructure.TestServices;
1017

1118
namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
@@ -354,6 +361,156 @@ public async Task When_GroupName_Default_Two_Containers()
354361
Assert.IsFalse(radioButtonNull2.IsChecked);
355362
}
356363

364+
[TestMethod]
365+
[RunsOnUIThread]
366+
public async Task When_RadioButtons_Unload_SelectedIndex()
367+
{
368+
var vm = new When_RadioButtons_Unload_VM
369+
{
370+
Items = ["QweQwe", "AsdAsd", "ZxcZxc"],
371+
CurrentIndex = 1,
372+
};
373+
374+
var sut = new RadioButtons
375+
{
376+
ItemsSource = vm.Items,
377+
};
378+
#if DEBUG
379+
// note: remember that with #22479 resetting data-bound properties on flyout closing,
380+
// observing with a breakpoint will have a side-effect: bp hit > app lost focus > flyout is closed > bug resetting radio dp
381+
vm.PropertyChangingEx += (s, e) =>
382+
{
383+
System.Diagnostics.Debug.WriteLine($"VM::{e.Property}: {e.OldValue} -> {e.NewValue}");
384+
385+
if (e is { Property: nameof(When_RadioButtons_Unload_VM.CurrentIndex), NewValue: -1 }) { }
386+
};
387+
#endif
388+
sut.SetBinding(RadioButtons.SelectedIndexProperty, new Binding
389+
{
390+
Path = new PropertyPath(nameof(When_RadioButtons_Unload_VM.CurrentIndex)),
391+
Mode = BindingMode.TwoWay,
392+
Source = vm,
393+
});
394+
var flyout = new Flyout
395+
{
396+
Content = new Grid { Children = { sut } }
397+
};
398+
var dropdown = new DropDownButton
399+
{
400+
Content = "sadsadasd",
401+
Flyout = flyout,
402+
};
403+
404+
WindowHelper.WindowContent = dropdown;
405+
await WindowHelper.WaitForLoaded(dropdown);
406+
407+
try
408+
{
409+
// 0. Open the flyout —- initial selection should be @1
410+
flyout.ShowAt(dropdown);
411+
await WindowHelper.WaitForLoaded(sut);
412+
await WindowHelper.WaitForIdle();
413+
Assert.AreEqual(1, sut.SelectedIndex, "0. Initial 'sut.SelectedIndex' should be 1");
414+
Assert.AreEqual(1, vm.CurrentIndex, "0. Initial 'vm.CurrentIndex' should be 1");
415+
416+
// 1. Select the last item @2 programmatically -- ...
417+
sut.SelectedIndex = 2;
418+
await WindowHelper.WaitForIdle();
419+
Assert.AreEqual(2, vm.CurrentIndex, "1. 'vm.CurrentIndex' should've been changed to 2");
420+
421+
// 2. Close the flyout —- selection should be preserved
422+
flyout.Hide();
423+
await WindowHelper.WaitForIdle();
424+
Assert.AreEqual(2, vm.CurrentIndex, "2. 'vm.CurrentIndex' should be preserved as 2");
425+
426+
// 3. Re-open the flyout —- selection should remain unchanged
427+
flyout.ShowAt(dropdown);
428+
await WindowHelper.WaitForLoaded(sut);
429+
await WindowHelper.WaitForIdle();
430+
Assert.AreEqual(2, sut.SelectedIndex, "3. 'sut.SelectedIndex' should remain unchanged as 2");
431+
Assert.AreEqual(2, vm.CurrentIndex, "3. 'vm.CurrentIndex' should remain unchanged as 2");
432+
}
433+
finally
434+
{
435+
UITestHelper.CloseAllPopups();
436+
}
437+
}
438+
439+
[TestMethod]
440+
[RunsOnUIThread]
441+
public async Task When_RadioButtons_Unload_SelectedItem()
442+
{
443+
var vm = new When_RadioButtons_Unload_VM();
444+
{
445+
vm.Items = ["QweQwe", "AsdAsd", "ZxcZxc"];
446+
vm.CurrentItem = vm.Items[1];
447+
}
448+
449+
var sut = new RadioButtons
450+
{
451+
ItemsSource = vm.Items,
452+
};
453+
#if DEBUG
454+
// note: remember that with #22479 resetting data-bound properties on flyout closing,
455+
// observing with a breakpoint will have a side-effect: bp hit > app lost focus > flyout is closed > bug resetting radio dp
456+
vm.PropertyChangingEx += (s, e) =>
457+
{
458+
System.Diagnostics.Debug.WriteLine($"VM::{e.Property}: {e.OldValue} -> {e.NewValue}");
459+
460+
if (e is { Property: nameof(When_RadioButtons_Unload_VM.CurrentItem), NewValue: null }) { }
461+
};
462+
#endif
463+
sut.SetBinding(RadioButtons.SelectedItemProperty, new Binding
464+
{
465+
Path = new PropertyPath(nameof(When_RadioButtons_Unload_VM.CurrentItem)),
466+
Mode = BindingMode.TwoWay,
467+
Source = vm,
468+
});
469+
var flyout = new Flyout
470+
{
471+
Content = new Grid { Children = { sut } }
472+
};
473+
var dropdown = new DropDownButton
474+
{
475+
Content = "sadsadasd",
476+
Flyout = flyout,
477+
};
478+
479+
WindowHelper.WindowContent = dropdown;
480+
await WindowHelper.WaitForLoaded(dropdown);
481+
482+
try
483+
{
484+
// 0. Open the flyout —- initial selection should be Items@1
485+
flyout.ShowAt(dropdown);
486+
await WindowHelper.WaitForLoaded(sut);
487+
await WindowHelper.WaitForIdle();
488+
Assert.AreEqual(vm.Items[1], sut.SelectedItem, $"0. Initial 'sut.SelectedItem' should be '{vm.Items[1]}'");
489+
Assert.AreEqual(vm.Items[1], vm.CurrentItem, $"0. Initial 'vm.CurrentItem' should be '{vm.Items[1]}'");
490+
491+
// 1. Select the last item @2 programmatically --- ...
492+
sut.SelectedItem = vm.Items[2];
493+
await WindowHelper.WaitForIdle();
494+
Assert.AreEqual(vm.Items[2], vm.CurrentItem, $"1. 'vm.CurrentItem' should've been changed to '{vm.Items[2]}'");
495+
496+
// 2. Close the flyout —- selection should be preserved
497+
flyout.Hide();
498+
await WindowHelper.WaitForIdle();
499+
Assert.AreEqual(vm.Items[2], vm.CurrentItem, $"2. 'vm.CurrentItem' should be preserved as '{vm.Items[2]}'");
500+
501+
// 3. Re-open the flyout —- selection should remain unchanged
502+
flyout.ShowAt(dropdown);
503+
await WindowHelper.WaitForLoaded(sut);
504+
await WindowHelper.WaitForIdle();
505+
Assert.AreEqual(vm.Items[2], sut.SelectedItem, $"3. 'sut.SelectedItem' should remain unchanged as '{vm.Items[2]}'");
506+
Assert.AreEqual(vm.Items[2], vm.CurrentItem, $"3. 'vm.CurrentItem' should remain unchanged as '{vm.Items[2]}'");
507+
}
508+
finally
509+
{
510+
UITestHelper.CloseAllPopups();
511+
}
512+
}
513+
357514
[TestMethod]
358515
public async Task When_AutomationPeer_Toggle()
359516
{
@@ -412,6 +569,47 @@ await RunOnUIThread(async () =>
412569
}
413570
}
414571

572+
public class When_RadioButtons_Unload_VM : INotifyPropertyChanged//, INotifyPropertyChanging
573+
{
574+
public event PropertyChangedEventHandler PropertyChanged;
575+
public event TypedEventHandler<object, (string Property, object OldValue, object NewValue)> PropertyChangingEx;
576+
577+
private int _currentIndex;
578+
private object _currentItem;
579+
private ObservableCollection<string> _items;
580+
581+
public int CurrentIndex
582+
{
583+
get => _currentIndex;
584+
set
585+
{
586+
PropertyChangingEx?.Invoke(this, (nameof(CurrentIndex), _currentIndex, value));
587+
_currentIndex = value;
588+
PropertyChanged?.Invoke(this, new(nameof(CurrentIndex)));
589+
}
590+
}
591+
public object CurrentItem
592+
{
593+
get => _currentItem;
594+
set
595+
{
596+
PropertyChangingEx?.Invoke(this, (nameof(CurrentItem), _currentItem, value));
597+
_currentItem = value;
598+
PropertyChanged?.Invoke(this, new(nameof(CurrentItem)));
599+
}
600+
}
601+
public ObservableCollection<string> Items
602+
{
603+
get => _items;
604+
set
605+
{
606+
PropertyChangingEx?.Invoke(this, (nameof(Items), _items, value));
607+
_items = value;
608+
PropertyChanged?.Invoke(this, new(nameof(Items)));
609+
}
610+
}
611+
}
612+
415613
public class TestCommand : ICommand
416614
{
417615
public event EventHandler CanExecuteChanged { add { } remove { } }

src/Uno.UI/UI/Xaml/Controls/RadioButtons/RadioButtons.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,15 @@ private void OnRepeaterElementClearing(ItemsRepeater itemsRepeater, ItemsRepeate
336336

337337
if (SharedHelpers.IsTrue(elementAsToggle.IsChecked))
338338
{
339-
Select(-1);
339+
#if HAS_UNO
340+
// Only reset selection if the item was actually removed from the source.
341+
// When the repeater unloads, Uno sends a fake Reset to mark containers as recyclable,
342+
// but the source is unchanged and selection should be preserved.
343+
if (IsInLiveTree)
344+
#endif
345+
{
346+
Select(-1);
347+
}
340348
}
341349
}
342350
}

0 commit comments

Comments
 (0)