일단 아래처럼 만들어 보았고, 질문있으시면 댓글달아주세요.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
namespace YegunKim
{
/// <summary>
/// Observable 하면서 Max 가 정해진 Queue 입니다.
/// </summary>
/// <typeparam name="T">Queue 가 사용할 데이터 타입</typeparam>
/// <see cref="https://referencesource.microsoft.com/#system/compmod/system/collections/generic/queue.cs"/>
/// <see cref="https://referencesource.microsoft.com/#system/compmod/system/collections/objectmodel/observablecollection.cs"/>
public class ObservableMaxQueue<T> : Queue<T>, INotifyPropertyChanged, INotifyCollectionChanged
{
private readonly int _capacity;
#region INotifyPropertyChanged
// MVVM 패턴에서 정상적으로 동작하려면 반드시 필요한 이벤트입니다.
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
#endregion
#region INotifyCollectionChanged
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
}
private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
}
private void OnCollectionReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
public ObservableMaxQueue(int capacity) : base(capacity)
{
_capacity = capacity;
}
public bool TryEnqueue(T item)
{
try
{
Enqueue(item);
}
catch (InvalidOperationException)
{
return false;
}
return true;
}
// Queue<T> 의 Enqueue 는 virtual 이 아니라 상속할 수 없지만, 아래처럼 base 와 new 를 사용하여 속이는 건 가능합니다.
public new void Enqueue(T item)
{
if (Contains(item))
throw new InvalidOperationException($"{nameof(ObservableMaxQueue<T>)} already contains {item}.");
if(_capacity < Count + 1)
throw new InvalidOperationException($"{nameof(ObservableMaxQueue<T>)} is already full.");
base.Enqueue(item);
OnPropertyChanged(nameof(Count));
// 여기서 애매한게 index 인데, Queue 소스코드를 보면 _array[_tail] 에 삽입하니 Count 로 설정했습니다.
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, Count);
}
public new T Dequeue()
{
var item = base.Dequeue();
OnPropertyChanged(nameof(Count));
// Enqueue 와 마찬가지로 Queue 소스코드를 보면 _array[_head] 에서 꺼내니 0 으로 설정했습니다.
OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, 0);
return item;
}
public new void Clear()
{
base.Clear();
OnCollectionReset();
}
public new void CopyTo(T[] array, int arrayIndex)
{
throw new NotImplementedException("다른 메서드를 참고해서 구현하시면 됩니다.");
}
}
}
아래처럼 테스트 코드도 작성했고 테스트도 통과했습니다.
using NUnit.Framework;
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using YegunKim;
namespace Test
{
public class ObservableMaxQueueUnitTests
{
[Test]
public void TestBasic()
{
var queue = new ObservableMaxQueue<int>(5);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
queue.Enqueue(4);
queue.Enqueue(5);
Assert.AreEqual(5, queue.Count);
Assert.AreEqual(1, queue.Dequeue());
Assert.AreEqual(2, queue.Dequeue());
Assert.AreEqual(3, queue.Dequeue());
Assert.AreEqual(4, queue.Dequeue());
Assert.AreEqual(5, queue.Dequeue());
Assert.AreEqual(0, queue.Count);
}
[Test]
public void TestError()
{
var queue = new ObservableMaxQueue<int>(5);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
queue.Enqueue(4);
queue.Enqueue(5);
Assert.Throws<InvalidOperationException>(() => queue.Enqueue(6));
Assert.AreEqual(5, queue.Count);
Assert.False(queue.TryEnqueue(6));
Assert.AreEqual(5, queue.Count);
Assert.AreEqual(1, queue.Dequeue());
}
[Test]
public void TestPropertyChanged()
{
var queue = new ObservableMaxQueue<int>(5);
var history = AssertExtensions.Raises<PropertyChangedEventArgs>(queue, nameof(queue.PropertyChanged), () =>
{
queue.Enqueue(1);
});
Assert.AreEqual(1, history.Count);
Assert.AreEqual(queue, history[0].Sender);
Assert.AreEqual(nameof(history.Count), history[0].EventArgs.PropertyName);
}
[Test]
public void TestCollectionChanged()
{
var queue = new ObservableMaxQueue<int>(5);
var history = AssertExtensions.Raises<NotifyCollectionChangedEventArgs>(queue, nameof(queue.CollectionChanged), () =>
{
queue.Enqueue(1);
});
Assert.AreEqual(1, history.Count);
Assert.AreEqual(queue, history[0].Sender);
Assert.AreEqual(NotifyCollectionChangedAction.Add, history[0].EventArgs.Action);
Assert.NotNull(history[0].EventArgs.NewItems);
Assert.AreEqual(1, history[0].EventArgs.NewItems.Count);
Assert.AreEqual(1, history[0].EventArgs.NewItems[0]);
history = AssertExtensions.Raises<NotifyCollectionChangedEventArgs>(queue, nameof(queue.CollectionChanged), () =>
{
queue.Dequeue();
});
Assert.AreEqual(1, history.Count);
Assert.AreEqual(queue, history[0].Sender);
Assert.AreEqual(NotifyCollectionChangedAction.Remove, history[0].EventArgs.Action);
Assert.NotNull(history[0].EventArgs.OldItems);
Assert.AreEqual(1, history[0].EventArgs.OldItems.Count);
Assert.AreEqual(1, history[0].EventArgs.OldItems[0]);
}
}
}
MVVM 패턴에서도 잘 동작하는지는 테스트를 하지 못해서… 해보시고 문제있으시면 알려주세요. 1시간 만에 작성하고 테스트한 코드라서 빠트린 부분이 있을 수 있습니다.
P.S @SangHyeon.Kim 님이 제시하신 것처럼 중복이 안된다는 의미로 클래스 이름에 Set 이나 Unique 을 포함시켜야 하지 않나 고민되네요…