ObservableCollection<T> : Represents a dynamic data collection that provides notifications when items get added, when items get removed, or when the whole list is refreshed.
ReadOnlyObservableCollection<T> : Represents a read-only version of ObservableCollection<T>.
Working with ObservableCollection<T>
ObservableCollection<T> is identical to working with List<T>, given that both of these classes implement the same core interfaces. What makes the ObservableCollection<T> class unique is that this class supports an event named CollectionChanged. This even will fire whenever a new item is inserted, a current item is removed (or relocated), or the entire collection is modified.
ObservableCollection<Person> p = new ObservableCollection<Person>()
{
new Person{ FirstName="Peter",LastName="Murphy",Age=52},
new Person{ FirstName="Kevin",LastName="Key",Age=48}
};
// wire up the CollectionChanged Event.
p.CollectionChanged += people_CollectionChanged;
p.Add(new Person() { FirstName = "Kevin", LastName = "Key", Age = 48 });
private static void people_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
Console.WriteLine("Add");
}
}
The incoming NotifyCollectionChangedEventArgs parameter defines 2 important properties. OldItems and NewItems, which will give you a list of items that were currently in the collection before the event fired and the new items that were involved in the change.
public enum NotifyCollectionChangedAction
{
Add,
Remove,
Replace,
Move,
Reset
}Creating Custom Generic Methods
The first step is to build a generic swap method.
When you build custom generic methods, you achieved a supercharged version of a traditional method overloading.
static class SwapFunctions
{
static void Swap(ref int a ,ref int b)
{
int temp = a;
a = b;
b = temp;
}
}
Adding overloading method
static void Swap(ref Person a, ref Person b)
{
Person temp = a;
a = b;
b = temp;
}
Whenever you have a group of overloaded methods that differ only by incoming arguments, this is your clue that generics could make your life easier.
static void Swap<T>(ref T a,ref T b)
{
T temp = a;
a = b;
b = temp;
}
string s1 = "Hello", s2 = "There";
SwapFunctions.Swap<string>(ref s1, ref s2);
Inference of Type Parameters
When you involve generic methods such as Swap<T>, you can optionally omit the type parameter if (and only if) the generic method required arguments because the compiler can infer the type parameter based on the member parameters.
bool b1 = true, b2 = false;
Console.WriteLine($"Before swap {b1} {b2}");
SwapFunctions.Swap(ref b1, ref b2);
Console.WriteLine($"After swap {b1} {b2}");
Even though the compiler can discover the correct type parameter based on the data type used to declare b1 and b2, you should get in the habit of always specifying the type parameter explicitly.
SwapFunctions.Swap<bool>(ref b1, ref b2);
static void DisplayBaseClass<T>()
{
Console.WriteLine(typeof(T)+" "+typeof(T).BaseType);
}DisplayBaseClass<int>();
DisplayBaseClass<string>();
Creating Custom Generic Structures and Classes
// A generic point structure
public struct Point<T>
{
// Generic state data
private T _xPos;
private T _yPos;
public Point(T xVal, T yVal)
{
_xPos = xVal;
_yPos = yVal;
}
public T X
{
get => _xPos;
set => _xPos = value;
}
public T Y
{
get => _yPos;
set => _yPos = value;
}
}
Point<int> p = new Point<int>(10,10);
Default Value Expressions with Generics
With the introduction of generics, the C# "default" keyword has been given a dual identity.
In addition to its use within a switch construct, it can be used to set a type parameter to its default value. This is helpful because a generic type does parameter to its default value. This is helpful because a generic type does not know the actual placeholders up front, which means it cannot safely assume what the default value will be. The defaults for a type parameter are as follows:
- Numeric values have a default value of 0.
- Reference types have a default value of null.
- Fields of a structure are set 0 (for value types) or null (for reference types).
Add this method to Point<T>
public void ResetPoint()
{
_xPos = default(T);
_yPos = default(T);
}
Default Literal Expressions
In addition to setting the default value of a property, C# 7.1 introduced default literal expressions.
public void ResetPoint()
{
_xPos = default(T);
_yPos = default(T);
}
Pattern Matching with Generics
Another update in C# 7.1, is the ability to pattern match on generics.
static void PatternMatching<T>(Point<T> p)
{
switch (p)
{
case Point<string> pString:
Console.WriteLine("Point is tring");
return;
case Point<int> pInt:
Console.WriteLine("Point is int");
return;
}
}
Point<string> p4 = default;
Point<int> p5 = default;
PatternMatching(p4);
PatternMatching(p5);
Constraining Type Parameters
Any generic item has at least one type parameter that you need to specify at the time you interact with the generic type or member. This alone allows you to build same type-safe code. However, you can also use the "where" keyword to get extremely specific about what a given type parameter must look like.
Using this keyword, you can add a set of constraints to a given type parameter, which the C# compiler will check at compile time.
- where T : struct => The type parameter<T> must have System.ValueType inits chain of inheritance (i.e. <T> must be structure)
- where T : class => The type parameter<T> must not have System.ValueType inits chain of inheritance (i.e. <T> must be a reference type).
- where T : new() => The type parameter <T> must have a default constructor. This is helpful if your generic type must create an instance of the type parameter because you cannot assume you know the format of custom constructors. Note that this constraint must be listed last on a multiconstrained type.
- where T : NameOfBaseClass => The type parameter <T> must be derived from the class specified by NameOfBaseClass.
- where T : NameOfInterface => The type parameter <T> must be derived from the interface specified by NameOfInterface. You can separate multiple interfaces as a comma-delimited list.
Examples of Using the Where Keyword
// MyGenericClass derives from object, while contained items must have a default ctor.
public class MyGenericClass<T> where T : new(){}
// MyGenericClass derives from object, while contained items must be a class implementing IDrawable and must support a default ctor.
public class MyGenericClass<T> where T : class, IDrawable, new() { }
// Error! new() constaint must be listed last!
public class MyGenericClass<T> where T : new(), class, IDrawable{}
// <K> must extend SomeBaseClass and have a default ctor,
// while <T> must be a structure and implement the generic IComparable interface.
public class MyGenericClass<K,T> where K : SomeBaseClass,new() where T : struct, IComparable<T> { }
// This method will swap any structure, but not classes.
public void Swap<T>(ref T a, ref T b) where T : struct { }
The Lack of Operator Constaints
You will get a compiler error if you apply any C# operators (+,-,*,==,etc) on the type parameters.
// Compiler error!
// Cannot apply operators to type parameters.
public class BasicMath<T>
{
public T Add(T arg1, T arg2) { return arg1 + arg2; }
public T Subtract(T arg1, T arg2) { return arg1 - arg2; }
public T Multiply(T arg1, T arg2) { return arg1 * arg2; }
public T Divide(T arg1, T arg2) { return arg1 / arg2; }
}
Do it for working:
// Illustrive code only!
public class BasicMath<T> where T : operator +,operator -, operator *,operator /
{
public T Add(T arg1, T arg2) { return arg1 + arg2; }
public T Subtract(T arg1, T arg2) { return arg1 - arg2; }
public T Multiply(T arg1, T arg2) { return arg1 * arg2; }
public T Divide(T arg1, T arg2) { return arg1 / arg2; }
}