Skip to content

Commit 6ad18aa

Browse files
Keboow-syss
andauthored
Improve duplicate handling (#2240)
* [Snackbar 1/3] Improve duplicate handling Renamed IgnoreDuplicate to ShowAlways. Added MessageExpired method. Added appropriate Equals method. Added autogenerated GetHashCode method. ShowAlways more closely represented what that property actually does. IgnoreDuplicate is not very telling in the context of a SnachkBarMessageQueueItem. MessageExpired serves as a helper to determine if a message already exceeded it's duration. Equals and HashCode simplify comparisons of SnackBarMessageQueueItems, especially useful in the determining if a message is a duplicate. [Snackbar 2/3] Improve duplicate handling Changed IgnoreDuplicate to DiscardDuplicates This reflects the changes to the Snackbar. Negating the selection is not necessary anymore. [Snackbar 3/3] Improve duplicate handling Changed IgnoreDuplicate to DiscardDuplicates. Extracted message checking. These changes improve readability of the SnackbarMessage filtering. Renamed ShowAlways to AlwaysShow. Removed null check for value type. Improved null handling in Equals. Simplyfied GetHashCode. Fixed typo in trace warning. Added check for empty collection. Changed Visibility to public. This helps testing the SnackbarMessageQueue. Added unit tests for SnackbarMessageQueue These are some basic unit tests for the SnackbarMessageQueue. This commit only tests the GetSnackbarMessage method if it handles duplicates and null values correctly. Moving System.ValueTuple to a paket source for net45 Updated NuGet dependency Co-authored-by: Ole Wiedemann <[email protected]> * Updates after rebase * Removing LastShownAt * Cleaning up build warnings Co-authored-by: Ole Wiedemann <[email protected]>
1 parent 01572d4 commit 6ad18aa

File tree

6 files changed

+153
-41
lines changed

6 files changed

+153
-41
lines changed

MainDemo.Wpf/Snackbars.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private void SnackBar4_OnClick(object sender, RoutedEventArgs e)
2424
{
2525
if (SnackbarFour.MessageQueue is { } messageQueue)
2626
{
27-
messageQueue.IgnoreDuplicate = !(DiscardDuplicateCheckBox.IsChecked ?? false);
27+
SnackbarFour.MessageQueue.DiscardDuplicates = DiscardDuplicateCheckBox.IsChecked ?? false;
2828
foreach (var s in ExampleFourTextBox.Text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
2929
{
3030
messageQueue.Enqueue(

MaterialDesignThemes.Wpf.Tests/MaterialDesignThemes.Wpf.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project Sdk="Microsoft.NET.Sdk">
33
<PropertyGroup>
4-
<TargetFrameworks>net472;net5.0-windows</TargetFrameworks>
4+
<TargetFrameworks>net472;netcoreapp3.1;net5.0-windows</TargetFrameworks>
55
<AssemblyTitle>MaterialDesignThemes.Wpf.Tests</AssemblyTitle>
66
<Product>MaterialDesignThemes.Wpf.Tests</Product>
77
</PropertyGroup>

MaterialDesignThemes.Wpf.Tests/SnackbarMessageQueueItemTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public class SnackbarMessageQueueItemTests
99
public void IsDuplicate_ThrowsOnNullArgument()
1010
{
1111
SnackbarMessageQueueItem item = CreateItem();
12-
Assert.Throws<ArgumentNullException>(() => item.IsDuplicate(null!));
12+
var ex = Assert.Throws<ArgumentNullException>(() => item.IsDuplicate(null!));
13+
Assert.Equal("value", ex.ParamName);
1314
}
1415

1516
[Fact]
@@ -22,9 +23,9 @@ public void IsDuplicate_WithDuplicateItems_ItReturnsTrue()
2223
}
2324

2425
[Fact]
25-
public void IsDuplicate_IgnoreDuplicatesIsTrue_ItReturnsFalse()
26+
public void IsDuplicate_AlwaysShowIsTrue_ItReturnsFalse()
2627
{
27-
SnackbarMessageQueueItem item = CreateItem(ignoreDuplicate:true);
28+
SnackbarMessageQueueItem item = CreateItem(alwaysShow:true);
2829
SnackbarMessageQueueItem other = CreateItem();
2930

3031
Assert.False(item.IsDuplicate(other));
@@ -48,11 +49,10 @@ public void IsDuplicate_WithDifferentActionContent_ItReturnsFalse()
4849
Assert.False(item.IsDuplicate(other));
4950
}
5051

51-
5252
private static SnackbarMessageQueueItem CreateItem(
5353
string content = "Content",
5454
string? actionContent = null,
55-
bool ignoreDuplicate = false)
56-
=> new SnackbarMessageQueueItem(content, TimeSpan.Zero, actionContent: actionContent, ignoreDuplicate: ignoreDuplicate);
55+
bool alwaysShow = false)
56+
=> new(content, TimeSpan.Zero, actionContent: actionContent, alwaysShow: alwaysShow);
5757
}
5858
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.ComponentModel;
5+
using System.Windows.Threading;
6+
using Xunit;
7+
8+
namespace MaterialDesignThemes.Wpf.Tests
9+
{
10+
public class SnackbarMessageQueueTests
11+
{
12+
private readonly SnackbarMessageQueue _snackbarMessageQueue;
13+
private readonly Dispatcher _dispatcher;
14+
15+
public SnackbarMessageQueueTests()
16+
{
17+
_dispatcher = Dispatcher.CurrentDispatcher;
18+
_snackbarMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(3), _dispatcher);
19+
}
20+
21+
[StaFact]
22+
[Description("Ensures that GetSnackbarMessage raises an exception on null values")]
23+
public void GetSnackbarMessageNullValues()
24+
{
25+
Assert.Throws<ArgumentNullException>(() => _snackbarMessageQueue.Enqueue(null!));
26+
Assert.Throws<ArgumentNullException>(() => _snackbarMessageQueue.Enqueue("", null, null));
27+
Assert.Throws<ArgumentNullException>(() => _snackbarMessageQueue.Enqueue(null!, "", null));
28+
Assert.Throws<ArgumentNullException>(() => _snackbarMessageQueue.Enqueue(null!, null, new Action(() => { })));
29+
}
30+
31+
[StaFact]
32+
[Description("Ensures that GetSnackbaMessage behaves correctly if the queue should discard duplicate items")]
33+
public void GetSnackbarMessageDiscardDuplicatesQueue()
34+
{
35+
_snackbarMessageQueue.DiscardDuplicates = true;
36+
37+
var firstItem = new object[] { "String & Action content", "Action content" };
38+
var secondItem = new object[] { "String & Action content", "Action content" };
39+
var thirdItem = new object[] { "Different String & Action content", "Action content" };
40+
41+
_snackbarMessageQueue.Enqueue(firstItem[0], firstItem[1], new Action(() => { }));
42+
_snackbarMessageQueue.Enqueue(secondItem[0], secondItem[1], new Action(() => { }));
43+
_snackbarMessageQueue.Enqueue(thirdItem[0], thirdItem[1], new Action(() => { }));
44+
45+
IReadOnlyList<SnackbarMessageQueueItem> messages = _snackbarMessageQueue.QueuedMessages;
46+
47+
Assert.Equal(2, messages.Count);
48+
49+
Assert.Equal("String & Action content", messages[0].Content);
50+
Assert.Equal("Action content", messages[0].ActionContent);
51+
Assert.Equal("Different String & Action content", messages[1].Content);
52+
Assert.Equal("Action content", messages[1].ActionContent);
53+
}
54+
55+
private class SnackbarMessageQueueSimpleTestData : IEnumerable<object[]>
56+
{
57+
public IEnumerator<object[]> GetEnumerator()
58+
{
59+
yield return new object[] { "String & Action content", "Action content" };
60+
yield return new object[] { "Different String & Action content", "Action content" };
61+
yield return new object[] { "Different String & Action content", "Action content" };
62+
yield return new object[] { "", "" };
63+
}
64+
65+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
66+
}
67+
68+
[StaTheory]
69+
[ClassData(typeof(SnackbarMessageQueueSimpleTestData))]
70+
[Description("Ensures that GetSnackbaMessage behaves correctly if the queue simply outputs items")]
71+
public void GetSnackbarMessageSimpleQueue(object content, object actionContent)
72+
{
73+
_snackbarMessageQueue.DiscardDuplicates = false;
74+
75+
_snackbarMessageQueue.Enqueue(content, actionContent, new Action(() => { }));
76+
77+
IReadOnlyList<SnackbarMessageQueueItem> messages = _snackbarMessageQueue.QueuedMessages;
78+
79+
Assert.Equal(1, messages.Count);
80+
81+
Assert.Equal(content, messages[0].Content);
82+
Assert.Equal(actionContent, messages[0].ActionContent);
83+
}
84+
}
85+
}

MaterialDesignThemes.Wpf/SnackbarMessageQueue.cs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,26 @@ public class SnackbarMessageQueue : ISnackbarMessageQueue, IDisposable
1414
{
1515
private readonly Dispatcher _dispatcher;
1616
private readonly TimeSpan _messageDuration;
17-
private readonly HashSet<Snackbar> _pairedSnackbars = new HashSet<Snackbar>();
18-
private readonly LinkedList<SnackbarMessageQueueItem> _snackbarMessages = new LinkedList<SnackbarMessageQueueItem>();
19-
private readonly object _snackbarMessagesLock = new object();
20-
private readonly ManualResetEvent _disposedEvent = new ManualResetEvent(false);
21-
private readonly ManualResetEvent _pausedEvent = new ManualResetEvent(false);
22-
private readonly SemaphoreSlim _showMessageSemaphore = new SemaphoreSlim(1, 1);
17+
private readonly HashSet<Snackbar> _pairedSnackbars = new();
18+
private readonly LinkedList<SnackbarMessageQueueItem> _snackbarMessages = new();
19+
private readonly object _snackbarMessagesLock = new();
20+
private readonly ManualResetEvent _disposedEvent = new(false);
21+
private readonly ManualResetEvent _pausedEvent = new(false);
22+
private readonly SemaphoreSlim _showMessageSemaphore = new(1, 1);
2323
private int _pauseCounter;
2424
private bool _isDisposed;
2525

26+
public IReadOnlyList<SnackbarMessageQueueItem> QueuedMessages
27+
{
28+
get
29+
{
30+
lock (_snackbarMessagesLock)
31+
{
32+
return _snackbarMessages.ToList();
33+
}
34+
}
35+
}
36+
2637
/// <summary>
2738
/// If set, the active snackbar will be closed.
2839
/// </summary>
@@ -127,7 +138,7 @@ internal Action Pair(Snackbar snackbar)
127138
{
128139
if (snackbar is null) throw new ArgumentNullException(nameof(snackbar));
129140

130-
_pairedSnackbars.Add(snackbar);
141+
_pairedSnackbars.Add(snackbar);
131142

132143
return () => _pairedSnackbars.Remove(snackbar);
133144
}
@@ -147,10 +158,10 @@ internal Action Pause()
147158
}
148159

149160
/// <summary>
150-
/// Gets or sets a value that indicates whether this message queue displays messages without discarding duplicates.
151-
/// True to show every message even if there are duplicates.
161+
/// Gets or sets a value that indicates whether this message queue displays messages without discarding duplicates.
162+
/// False to show every message even if there are duplicates.
152163
/// </summary>
153-
public bool IgnoreDuplicate { get; set; }
164+
public bool DiscardDuplicates { get; set; }
154165

155166
public void Enqueue(object content) => Enqueue(content, false);
156167

@@ -178,8 +189,8 @@ public void Enqueue<TArgument>(object content, object? actionContent, Action<TAr
178189

179190
if (actionContent is null ^ actionHandler is null)
180191
{
181-
throw new ArgumentException("All action arguments must be provided if any are provided.",
182-
actionContent != null ? nameof(actionContent) : nameof(actionHandler));
192+
throw new ArgumentNullException(actionContent != null ? nameof(actionContent) : nameof(actionHandler),
193+
"All action arguments must be provided if any are provided.");
183194
}
184195

185196
Action<object?>? handler = actionHandler != null
@@ -195,8 +206,8 @@ public void Enqueue(object content, object? actionContent, Action<object?>? acti
195206

196207
if (actionContent is null ^ actionHandler is null)
197208
{
198-
throw new ArgumentException("All action arguments must be provided if any are provided.",
199-
actionContent != null ? nameof(actionContent) : nameof(actionHandler));
209+
throw new ArgumentNullException(actionContent != null ? nameof(actionContent) : nameof(actionHandler),
210+
"All action arguments must be provided if any are provided.");
200211
}
201212

202213
var snackbarMessageQueueItem = new SnackbarMessageQueueItem(content, durationOverride ?? _messageDuration,
@@ -212,8 +223,7 @@ private void InsertItem(SnackbarMessageQueueItem item)
212223
var node = _snackbarMessages.First;
213224
while (node != null)
214225
{
215-
if (!IgnoreDuplicate && item.IsDuplicate(node.Value))
216-
return;
226+
if (DiscardDuplicates && item.IsDuplicate(node.Value)) return;
217227

218228
if (item.IsPromoted && !node.Value.IsPromoted)
219229
{
@@ -224,7 +234,9 @@ private void InsertItem(SnackbarMessageQueueItem item)
224234
node = node.Next;
225235
}
226236
if (!added)
237+
{
227238
_snackbarMessages.AddLast(item);
239+
}
228240

229241
}
230242

@@ -275,7 +287,6 @@ private void StartDuration(TimeSpan minimumDuration, EventWaitHandle durationPas
275287
});
276288
}
277289

278-
279290
private async Task ShowNextAsync()
280291
{
281292
await _showMessageSemaphore.WaitAsync()

MaterialDesignThemes.Wpf/SnackbarMessageQueueItem.cs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
using System;
2+
using System.Collections.Generic;
23

34
namespace MaterialDesignThemes.Wpf
45
{
5-
internal class SnackbarMessageQueueItem
6+
public class SnackbarMessageQueueItem
67
{
78
public SnackbarMessageQueueItem(object content,
89
TimeSpan duration,
910
object? actionContent = null,
1011
Action<object?>? actionHandler = null,
1112
object? actionArgument = null,
1213
bool isPromoted = false,
13-
bool ignoreDuplicate = false)
14+
bool alwaysShow = false)
1415
{
1516
Content = content;
1617
Duration = duration;
1718
ActionContent = actionContent;
1819
ActionHandler = actionHandler;
1920
ActionArgument = actionArgument;
2021
IsPromoted = isPromoted;
21-
IgnoreDuplicate = ignoreDuplicate;
22+
AlwaysShow = alwaysShow;
2223
}
2324

2425
/// <summary>
@@ -52,25 +53,40 @@ public SnackbarMessageQueueItem(object content,
5253
public bool IsPromoted { get; }
5354

5455
/// <summary>
55-
/// Still display this message even if it is a duplicate.
56+
/// Always show this message, even if it's a duplicate
5657
/// </summary>
57-
public bool IgnoreDuplicate { get; }
58+
public bool AlwaysShow { get; }
5859

59-
/// <summary>
60-
/// Checks if given item is a duplicate to this
61-
/// </summary>
62-
/// <param name="item">Item to check for duplicate</param>
63-
/// <returns><c>true</c> if given item is a duplicate to this, <c>false</c> otherwise</returns>
64-
public bool IsDuplicate(SnackbarMessageQueueItem item)
60+
public override bool Equals(object? obj)
61+
{
62+
if (obj is not SnackbarMessageQueueItem message)
63+
{
64+
return false;
65+
}
66+
67+
return EqualityComparer<object>.Default.Equals(Content, message.Content)
68+
&& EqualityComparer<object?>.Default.Equals(ActionContent, message.ActionContent);
69+
}
70+
71+
public override int GetHashCode()
72+
{
73+
unchecked
74+
{
75+
int rv = Content.GetHashCode();
76+
rv = (rv * 397) ^ (ActionContent?.GetHashCode() ?? 0);
77+
return rv;
78+
}
79+
}
80+
81+
public bool IsDuplicate(SnackbarMessageQueueItem value)
6582
{
66-
if (item is null)
83+
if (value is null)
6784
{
68-
throw new ArgumentNullException(nameof(item));
85+
throw new ArgumentNullException(nameof(value));
6986
}
7087

71-
return !IgnoreDuplicate
72-
&& Equals(item.Content, Content)
73-
&& Equals(item.ActionContent, ActionContent);
88+
if (AlwaysShow) return false;
89+
return Equals(value);
7490
}
7591
}
7692
}

0 commit comments

Comments
 (0)