An observable lists library with upcast compatibility, list wrapping, reentrancy protection, thread safety, and robust unit testing.
Observable Lists:
A set of lists/list wrappers/adapters that implement the observable pattern, invoking events when a list is modified.
ObservableList<TItem>
, ObservableIList<TItem,TList<TItem>>
, ObservableIListLocking<TItem,TList<TItem>>
, IObservableList<TItem>
Observable List Bindings: A set of list bindings that synchronize the content of observable lists using a mapping between item types.
ObservableListBind<TItemA,TItemB>
, ObservableListBindFunc<TItemA,TItemB>
, ObservableListBindProperty<TItemA,TItemB>
, ObservableListBindPropertyFunc<TItemA,TItemB>
Gstc.Collections.ObservableLists
Author: Greg Sonnenfeld, Copyright 2019 to 2023
Version: 2.0
License: LGPL 3.0
Nuget: https://www.nuget.org/packages/Gstc.Collections.ObservableLists
Version 2.0.1 (2023-03-18)
Features:
a. IReadOnlyList{T} added to IObservableList and implementations.
b. IList interface now does type checking similar to List{T}
c. IList interface added back into ObservableIListLocking
d. Utils folder added, with public SyncingFlagScope, ReentrancyMonitor and LockRwScope mechanism that were previously implemented internally in classes.
e. ObservableList{T} is now internally a type of ObservableList{T,List{T}}, addRange/Move are now ovveridable virtual methods.
Bugfixes:
a. ObservableListBind inProgress flag has been made exception safe.
b. int IList.Add(object) now calls IList_AddCustom(TItem) which is overridable. This allows custom index return values.
Minor breaking changes:
a. ObservableIListLocking moved to main namespace.
b. ObservableLists now throws ReentrancyException derived from InvalidOperationException, instead of InvalidOperationException
c. ObservableListBinds now throws added type OneWayBindingException derived from NotSupportedException, instead of NotSupportedException
d. The names of some abstract classes have been changed.
e. Names of internally locking mechanisms changed, refactored.
f. Switches some classes to use 'default' instead of 'null' for better compatibility with value types.
Other:
Updated unit tests.
Some benchmark code.
fixed typos.
Version 2.0.0 (2023-02-07)
Complete refactor of version 1.0.0. Check source and documentation for new functionality, as there are enough changes to constitute a new product.
The ObservableList<TItem>
, ObservableIList<TItem,TList<TItem>>
, ObservableIListLocking<TItem,TList<TItem>>
, provide IList<T>
implementations that invoke events ( OnCollectionChanged
, OnCollectionChanging
, Adding
, Added
, Moving
, Moved
, Removing
, Removed
, Replacing
, Replaced
,Resetting
, Reset
) when the list is modified. They provide a robust alternative to the .Net ObservableCollection<T>
.
ObservableList<TItem>
is the default observable list that utilizes an internal List<TItem>
and can also serve as a wrapper/adapter for a pre-existing List<T>
. It provides list modification events, maintains event calls on upcast, and provides reenetrancy protection.
var obvList = new ObservableList<Customer>();
obvList.Adding += (sender,args)=> Console.WriteLine("Attempting to add a new customer.");
ObservableIList<TItem,TList<TItem>>
is similar to ObservableList<TItem>
, but allows the user to specify the internal list type with the TList<TItem>
generic parameter.
Collection<Customer> customers = SomeDbApi.GetCustomers();
var obvList = new ObservableIList<Customer, Collection<Customer>>(customers);
obvList.Adding += (sender,args)=> Console.WriteLine("Adding new customer to database mapped collection.");
ObservableIListLocking<TItem,TList<TItem>>
is similar to ObservableIList<TItem,TList<TItem>>
, but implements a ReaderWriterLockSlim
list access, lock
for event access, and special reentrancy rules for asynchronous/multithread operation.
var obvList = new ObservableIList<Customer, Collection<Customer>>() {List = SomeExampleCollection;}
obvList.Adding += (sender,args)=> Console.WriteLine("Fetching customer from Web API...");
for (int index = 0; index < 1000; index++) Task task = Task.Run(() => obvList.Add( MyWebApi.getNewCustomer() ));
IObservableList<Item>
is the interfae for these classes and includes IList<T>
, IList
, ICollection<T>
, INotifyCollectionChanging
, INotifyCollectionChanged
, INotifyListChangingEvents
, INotifyListChangingEvents
ObservableListBind<TItemA,TItemB>
provides synchronization between two ObservableLists of different but related types <TItemA>
and <TItemB>
. List methods (Add, Remove, clear, etc) on one list is propogated to the other given a conversion method. ObservableListBindFunc<TItemA,TItemB>
is an implementation that allows the conversion method to be passed in the constructor as an anonymous function.
The <TItemA>
and <TItemB>
are classes that map to each other in an injective way, usually containing ommissions or data transformation. The user provides a ConvertItem(...)
method that provide a two way conversion between a <TItemA>
and <TItemB>
object. This is most often used when one needs to transform model data for display or a public API.
var obvListBind = new ObservableListBindFunc<int, string>(
(itemA) => itemA.ToString(),
(ItemB) => int.Parse(ItemB),
new ObservableList<int>(),
new ObservableList<string>()
);
ObservableListBindProperty<TItemA,TItemB>
provides the functionality of ObservableListBind<TItemA,TItemB>
and also provides synchronization between the properties of list item that implement INotifyPropertyChanged
.
//See Gstc.Collections.ObservableLists.ExampleTest for example usage.
UpdateCollectionNotify
- When a PropertyChanged event is raised, the corresponding item on the alternate list will be replaced by a new item created using the ConvertItem(...) method.
UpdatePropertyNotify
- When a PropertyChanged event is raised, the corresponding item on the alternate list will have its PropertyChanged event triggered. The user is expected to provide any property synchronization. This is useful when the ItemB is a a wrapper for ItemA, and a PropertyChanged event is needed to trigger callbacks.
UpdateCustomNotify
- When a PropertyChanged event is raised, the user provided ICustomPropertyMap
is invoked to update item property on the alternate list.
The ObservableList<T>
should work somewhat similar to the standard .NET ObservableCollection<T>
.
First, add the nuget package [ https://www.nuget.org/packages/Gstc.Collections.ObservableLists ] or checkout the code and include it in you project.
Next utilize code from the following examples or look at the github examples in the Gstc.Collections.ObservableLists.ExampleTest namespace!
The following example shows usage of an ObservableList<T>
:
ObservableList<Customer> obvCustomerList = new ObservableList<Customer>();
//An event added for collection changing
obvCustomerList.CollectionChanged += (sender, args) => {
if (args.Action == NotifyCollectionChangedAction.Reset)
foreach (Customer customer in (ObservableList<Customer>)sender)
Console.WriteLine("Initial Customers: " + customer.FirstName + " " + customer.LastName);
};
//An existing list is assigned to be the internal list
List<Customer> customerList = Customer.GenerateCustomerList();
obvCustomerList.List = customerList;
//The ObservableList has functionality of a normal IList<>/Enumerable<>/etc.
foreach (Customer item in obvCustomerList) Console.WriteLine("ObservableList has customer:" + item.FirstName);
// IObservableList has hooks specific to actions (add, remove, reset, replace, move) as well as OnCollectionChanged.
obvCustomerList.Adding += (sender, args) => {
foreach (Customer customer in args.NewItems)
Console.WriteLine("Going to add Customer: " + customer.FirstName + " " + customer.LastName);
};
obvCustomerList.Added += (sender, args) => {
foreach (Customer customer in args.NewItems)
Console.WriteLine("Customer was Added: " + customer.FirstName + " " + customer.LastName);
};
obvCustomerList.Add(Customer.GenerateCustomer());
// IObservableList<> can be used in external methods that implement IList<> and IList
void AddCustomerToList(IList<Customer> list) => list.Add(Customer.GenerateCustomer());
void AddCustomerToList2(IList list) => list.Add(Customer.GenerateCustomer());
AddCustomerToList(obvCustomerList);
AddCustomerToList2(obvCustomerList);
The following demonstrates how the ObservableListBind works. For examples of ObservableListBindProperty<TItemA,TItemB>
see the Gstc.Collections.ObservableLists.ExampleTest namespace.
public void ObservableListBindExample() {
ObservableList<PhoneViewModel> obvPhoneListVM = new(); // Empty list
ObservableList<PhoneModel> obvPhoneListM = new() { // Our example list with initial data
new() { PhoneNumber = 5551112222 },
new() { PhoneNumber = 5553334444 },
};
//Creates a binding between the two lists so they will have same content in converted form.
ObservableListBindPhone obvBindPhone = new(
obvListPhoneModel: obvPhoneListM,
obvListPhoneViewModel: obvPhoneListVM,
isBidirectional: true
);
foreach (var item in obvPhoneListM) Console.WriteLine(item.PhoneNumber);
foreach (var item in obvPhoneListVM) Console.WriteLine(item.PhoneString);
/// Output:
/// 5551112222
/// 5553334444
/// 555-111-2222
/// 555-333-4444
obvPhoneListVM.Clear();
Console.WriteLine(obvPhoneListM.Count);
Console.WriteLine(obvPhoneListVM.Count);
/// Output:
/// 0
/// 0
obvPhoneListM.Add(new() { PhoneNumber = 9876543210 });
obvPhoneListVM.Add(new() { PhoneString = "123-456-7890" });
foreach (var item in obvPhoneListM) Console.WriteLine(item.PhoneNumber);
foreach (var item in obvPhoneListVM) Console.WriteLine(item.PhoneString);
/// Output:
/// 9876543210
/// 1234567890
/// 987-654-3210
/// 123-456-7890
/// ObservableListBindFunc can be used as alternate to inheriting an abstract class ObservableListBind by passing
/// conversion functions in the constructor.
IObservableListBind<PhoneModel, PhoneViewModel> obvBindPhoneFunc
= new ObservableListBindFunc<PhoneModel, PhoneViewModel>(
convertItemAToB: (item) => new PhoneViewModel() { PhoneString = item.PhoneNumber.ToString("###-###-####") },
convertItemBToA: (item) => new PhoneModel() { PhoneNumber = long.Parse(Regex.Replace(item.PhoneString, "[^0-9]", "")) },
observableListA: new ObservableList<PhoneModel>() { new PhoneModel() { PhoneNumber = 1112223333 } },
observableListB: new ObservableList<PhoneViewModel>(),
isBidirectional: false,
sourceList: ListIdentifier.ListA
);
foreach (var item in obvBindPhoneFunc.ObservableListA) Console.WriteLine(item.PhoneNumber);
foreach (var item in obvBindPhoneFunc.ObservableListB) Console.WriteLine(item.PhoneString);
/// Output:
/// 1112223333
/// 111-222-3333
}
//This is the implmentation of the abstract class with a constructor and the convertItem(...) implemented.
public class ObservableListBindPhone : ObservableListBind<PhoneModel, PhoneViewModel> {
public ObservableListBindPhone(
IObservableList<PhoneModel> obvListPhoneModel,
IObservableList<PhoneViewModel> obvListPhoneViewModel,
bool isBidirectional)
: base(obvListPhoneModel, obvListPhoneViewModel, isBidirectional, ListIdentifier.ListA) { }
public override PhoneViewModel ConvertItem(PhoneModel item) => new() { PhoneString = item.PhoneNumber.ToString("###-###-####") };
public override PhoneModel ConvertItem(PhoneViewModel item) => new() { PhoneNumber = long.Parse(Regex.Replace(item.PhoneString, "[^0-9]", "")) };
}
public class PhoneModel {
public long PhoneNumber { get; set; }
}
public class PhoneViewModel {
public string PhoneString { get; set; }
}