Skip to content

Commit 913e6a8

Browse files
committed
Unset Parent on Conductors' Items.Clear()
ObservableCollection<T> is designed so NotifyCollectionChangedEventArgs.OldItems is not populated when .Clear() is called on the collection. Because of this, Caliburn conductors can't unset the Parent property on all conducted IChild. There are multiple solutions to this issue that have been discussed, but I feel like adding a new CollectionCleared event to BindableCollection is the best compromise considering it doesn't break any existing code and only extends existing ObservableCollection functionality.
1 parent 3db4048 commit 913e6a8

7 files changed

+76
-7
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Xunit;
2+
3+
namespace Caliburn.Micro.Core.Tests
4+
{
5+
public class BindableCollectionTests
6+
{
7+
[Fact]
8+
public void Clear_ThenCollectionClearedIsRaisedWithAllItems()
9+
{
10+
var objects = new[] { new object(), new object(), new object() };
11+
var bindableCollection = new BindableCollection<object>(objects);
12+
13+
var collectionClearedEvent = Assert.Raises<CollectionClearedEventArgs<object>>(
14+
h => bindableCollection.CollectionCleared += h, h => bindableCollection.CollectionCleared -= h,
15+
() => bindableCollection.Clear()
16+
);
17+
18+
Assert.Equal(objects, collectionClearedEvent.Arguments.ClearedItems);
19+
}
20+
}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Xunit;
2+
3+
namespace Caliburn.Micro.Core.Tests
4+
{
5+
public class ConductorCollectionAllActiveTests
6+
{
7+
[Fact]
8+
public void ParentItemIsUnsetOnClear()
9+
{
10+
var conductor = new Conductor<IScreen>.Collection.AllActive();
11+
var conducted = new[] { new Screen(), new Screen() };
12+
conductor.Items.AddRange(conducted);
13+
14+
conductor.Items.Clear();
15+
16+
Assert.All(conducted, screen => Assert.NotEqual(conductor, screen.Parent));
17+
}
18+
}
19+
}

src/Caliburn.Micro.Core.Tests/ConductorWithCollectionOneActiveTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task CanCloseIsTrueWhenItemsAreClosable()
4444
conductor.Items.Add(conducted);
4545

4646
await ((IActivate)conductor).ActivateAsync(CancellationToken.None);
47-
var canClose =await conductor.CanCloseAsync(CancellationToken.None);
47+
var canClose = await conductor.CanCloseAsync(CancellationToken.None);
4848

4949
Assert.True(canClose);
5050
Assert.False(conducted.IsClosed);
@@ -62,7 +62,7 @@ public async Task CanCloseIsTrueWhenItemsAreNotClosableAndCloseStrategyCloses()
6262
IsClosable = true
6363
};
6464
conductor.Items.Add(conducted);
65-
await((IActivate)conductor).ActivateAsync(CancellationToken.None);
65+
await ((IActivate)conductor).ActivateAsync(CancellationToken.None);
6666
var canClose = await conductor.CanCloseAsync(CancellationToken.None);
6767

6868
Assert.True(canClose);
@@ -119,7 +119,7 @@ public void ParentItemIsSetOnReplacedConductedItem()
119119
Assert.Equal(conductor, newConducted.Parent);
120120
}
121121

122-
[Fact(Skip = "This is not possible as we don't get the removed items in the event handler.")]
122+
[Fact]
123123
public void ParentItemIsUnsetOnClear()
124124
{
125125
var conductor = new Conductor<IScreen>.Collection.OneActive();

src/Caliburn.Micro.Core/BindableCollection.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public BindableCollection(IEnumerable<T> collection)
3535
/// </summary>
3636
public bool IsNotifying { get; set; }
3737

38+
/// <summary>
39+
/// Occurs when the collection has been cleared.
40+
/// </summary>
41+
public event EventHandler<CollectionClearedEventArgs<T>> CollectionCleared;
42+
3843
/// <summary>
3944
/// Notifies subscribers of the property change.
4045
/// </summary>
@@ -180,7 +185,9 @@ protected override sealed void ClearItems()
180185
/// </remarks>
181186
protected virtual void ClearItemsBase()
182187
{
188+
var clearedItems = new List<T>(collection: this);
183189
base.ClearItems();
190+
CollectionCleared?.Invoke(this, new CollectionClearedEventArgs<T>(clearedItems));
184191
}
185192

186193
/// <summary>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Caliburn.Micro
5+
{
6+
public class CollectionClearedEventArgs<T> : EventArgs
7+
{
8+
public CollectionClearedEventArgs(IReadOnlyCollection<T> clearedItems)
9+
{
10+
ClearedItems = clearedItems;
11+
}
12+
13+
public IReadOnlyCollection<T> ClearedItems { get; }
14+
}
15+
}

src/Caliburn.Micro.Core/ConductorWithCollectionAllActive.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Collections.Specialized;
43
using System.Linq;
54
using System.Reflection;
@@ -38,6 +37,10 @@ public AllActive(bool openPublicItems)
3837
/// </summary>
3938
public AllActive()
4039
{
40+
_items.CollectionCleared += (s, e) =>
41+
{
42+
e.ClearedItems.OfType<IChild>().Apply(x => x.Parent = null);
43+
};
4144
_items.CollectionChanged += (s, e) =>
4245
{
4346
switch (e.Action)
@@ -80,7 +83,7 @@ protected override Task OnActivateAsync(CancellationToken cancellationToken)
8083
/// <returns>A task that represents the asynchronous operation.</returns>
8184
protected override async Task OnDeactivateAsync(bool close, CancellationToken cancellationToken)
8285
{
83-
foreach(var deactivate in _items.OfType<IDeactivate>())
86+
foreach (var deactivate in _items.OfType<IDeactivate>())
8487
{
8588
await deactivate.DeactivateAsync(close, cancellationToken);
8689
}

src/Caliburn.Micro.Core/ConductorWithCollectionOneActive.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public class OneActive : ConductorBaseWithActiveItem<T>
2525
/// </summary>
2626
public OneActive()
2727
{
28+
_items.CollectionCleared += (s, e) =>
29+
{
30+
e.ClearedItems.OfType<IChild>().Apply(x => x.Parent = null);
31+
};
2832
_items.CollectionChanged += (s, e) =>
2933
{
3034
switch (e.Action)
@@ -172,7 +176,7 @@ public override async Task<bool> CanCloseAsync(CancellationToken cancellationTok
172176
closable = stillToClose;
173177
}
174178

175-
foreach(var deactivate in closable.OfType<IDeactivate>())
179+
foreach (var deactivate in closable.OfType<IDeactivate>())
176180
{
177181
await deactivate.DeactivateAsync(true, cancellationToken);
178182
}

0 commit comments

Comments
 (0)