Skip to content

Commit

Permalink
Remove some index -1 exceptions (#781)
Browse files Browse the repository at this point in the history
* Include index =0 for inital change set on source list. Fixes #723

* Add clone of AvaloniaDictionary to the test project for testing

* Add tests for Avalonia + throw the dreaded -1 exception. Fixes #710. Fixes #683 -> hopefully !

* Dear Editor.Config, you've got to be kidding me,  Telling me I should mark something with [Serializable] when that was a bad idea in 2004 and is totally evil in 2023. Binary formatting support required?? Please grow up and remove this as a suggestion to spare people the problem of having to turn it off.

* Add comment regarding avalonia dictionarytest

* Use UnspecifiedIndexException
  • Loading branch information
RolandPheasant committed Dec 5, 2023
1 parent bb8b5dc commit df66dd4
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_prefer_collection_expression = true:suggestion
dotnet_diagnostic.CA2237.severity = none

[project.json]
indent_size = 2
Expand Down Expand Up @@ -699,6 +700,7 @@ csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.RCS1194.severity = none

# C++ Files
[*.{cpp,h,in}]
Expand Down
311 changes: 311 additions & 0 deletions src/DynamicData.Tests/Binding/AvaloniaDictionaryFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

using DynamicData.Binding;
using DynamicData.Tests.Domain;
using FluentAssertions;
using Xunit;

namespace DynamicData.Tests.Binding;

public class AvaloniaDictionaryFixture
{
private readonly AvaloniaDictionary<string, Person> _collection;
private readonly ChangeSetAggregator<Person> _results;

public AvaloniaDictionaryFixture()
{
_collection = new AvaloniaDictionary<string, Person>();
_results = _collection.ToObservableChangeSet<AvaloniaDictionary<string, Person>, KeyValuePair<string, Person>>()
.Transform(x=>x.Value)
.AsAggregator();
}

[Fact]
public void Add()
{
var person = new Person("Someone",10, "M");

_collection.Add("Someone", person);

_results.Messages.Count.Should().Be(1);
_results.Data.Count.Should().Be(1);
_results.Data.Items.First().Should().Be(person);
}

[Fact]
public void Replace()
{
var person1 = new Person("Someone", 10, "M");
var person2 = new Person("Someone", 11, "M");

_collection.Add("Someone", person1);
_collection["Someone"] = person2;

_results.Data.Count.Should().Be(1);
_results.Data.Items.First().Should().Be(person2);
}


[Fact]
public void Remove()
{
var person = new Person("Someone", 10, "M");

_collection.Add("Someone", person);
_collection.Remove(person.Key);

_results.Data.Count.Should().Be(0);
}
}


public interface IAvaloniaDictionary<TKey, TValue>
: IDictionary<TKey, TValue>,
IAvaloniaReadOnlyDictionary<TKey, TValue>,
IDictionary
where TKey : notnull
{
}


public interface IAvaloniaReadOnlyDictionary<TKey, TValue>
: IReadOnlyDictionary<TKey, TValue>,
INotifyCollectionChanged,
INotifyPropertyChanged
where TKey : notnull
{
}


/*
Copied from Avalionia because an issue was raised due to compatibility issues with ToObservableChangeSet().
There's not other way of testing it.
See https://github.com/AvaloniaUI/Avalonia/blob/d7c82a1a6f7eb95b2214f20a281fa0581fb7b792/src/Avalonia.Base/Collections/AvaloniaDictionary.cs#L17
*/
public class AvaloniaDictionary<TKey, TValue> : IAvaloniaDictionary<TKey, TValue> where TKey : notnull
{
private Dictionary<TKey, TValue> _inner;

/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaDictionary{TKey, TValue}"/> class.
/// </summary>
public AvaloniaDictionary()
{
_inner = new Dictionary<TKey, TValue>();
}

/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaDictionary{TKey, TValue}"/> class.
/// </summary>
public AvaloniaDictionary(int capacity)
{
_inner = new Dictionary<TKey, TValue>(capacity);
}

/// <summary>
/// Occurs when the collection changes.
/// </summary>
public event NotifyCollectionChangedEventHandler? CollectionChanged;

/// <summary>
/// Raised when a property on the collection changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;

/// <inheritdoc/>
public int Count => _inner.Count;

/// <inheritdoc/>
public bool IsReadOnly => false;

/// <inheritdoc/>
public ICollection<TKey> Keys => _inner.Keys;

/// <inheritdoc/>
public ICollection<TValue> Values => _inner.Values;

bool IDictionary.IsFixedSize => ((IDictionary)_inner).IsFixedSize;

ICollection IDictionary.Keys => ((IDictionary)_inner).Keys;

ICollection IDictionary.Values => ((IDictionary)_inner).Values;

bool ICollection.IsSynchronized => ((IDictionary)_inner).IsSynchronized;

object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot;

IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => _inner.Keys;

IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => _inner.Values;

/// <summary>
/// Gets or sets the named resource.
/// </summary>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or null if not found.</returns>
public TValue this[TKey key]
{
get
{
return _inner[key];
}

set
{
bool replace = _inner.TryGetValue(key, out var old);
_inner[key] = value;

if (replace)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));

if (CollectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
new KeyValuePair<TKey, TValue>(key, value),
new KeyValuePair<TKey, TValue>(key, old!));
CollectionChanged(this, e);
}
}
else
{
NotifyAdd(key, value);
}
}
}

object? IDictionary.this[object key] { get => ((IDictionary)_inner)[key]; set => ((IDictionary)_inner)[key] = value; }

/// <inheritdoc/>
public void Add(TKey key, TValue value)
{
_inner.Add(key, value);
NotifyAdd(key, value);
}

/// <inheritdoc/>
public void Clear()
{
var old = _inner;

_inner = new Dictionary<TKey, TValue>();

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(CommonPropertyNames.IndexerName));


if (CollectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
old.ToArray(),
-1);
CollectionChanged(this, e);
}
}

/// <inheritdoc/>
public bool ContainsKey(TKey key) => _inner.ContainsKey(key);

/// <inheritdoc/>
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
((IDictionary<TKey, TValue>)_inner).CopyTo(array, arrayIndex);
}

/// <inheritdoc/>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _inner.GetEnumerator();

/// <inheritdoc/>
public bool Remove(TKey key)
{
if (_inner.TryGetValue(key, out var value))
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));

if (CollectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
new[] { new KeyValuePair<TKey, TValue>(key, value) },
-1);
CollectionChanged(this, e);
}

return true;
}
else
{
return false;
}
}

/// <inheritdoc/>
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _inner.TryGetValue(key, out value);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();

/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index) => ((ICollection)_inner).CopyTo(array, index);

/// <inheritdoc/>
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}

/// <inheritdoc/>
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
return _inner.Contains(item);
}

/// <inheritdoc/>
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}

/// <inheritdoc/>
void IDictionary.Add(object key, object? value) => Add((TKey)key, (TValue)value!);

/// <inheritdoc/>
bool IDictionary.Contains(object key) => ((IDictionary)_inner).Contains(key);

/// <inheritdoc/>
IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator();

/// <inheritdoc/>
void IDictionary.Remove(object key) => Remove((TKey)key);

private void NotifyAdd(TKey key, TValue value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));


if (CollectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
new[] { new KeyValuePair<TKey, TValue>(key, value) },
-1);
CollectionChanged(this, e);
}
}
}
internal static class CommonPropertyNames
{
public const string IndexerName = "Item";
}
24 changes: 24 additions & 0 deletions src/DynamicData.Tests/List/SourceListFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Xunit;

namespace DynamicData.Tests.List;

public class SourceListFixture
{
[Fact]
public void InitialChangeIsRange()
{
var source = new SourceList<string>();
source.Add("A");
var changeSets = new List<IChangeSet<string>>();

source.Connect().Subscribe(changeSets.Add).Dispose();


changeSets[0].First().Type.Should().Be(ChangeType.Range);
changeSets[0].First().Range.Index.Should().Be(0);
}
}
Loading

0 comments on commit df66dd4

Please sign in to comment.