Introduction
We frequently need to control (or limit) the public operations of an object based on the current state of another object (mostly the owner).
For example, you have a playlist class that has some properties, methods and a collection of playlist items. You want to prevent manipulation of the playlist item collection if player is playing this playlist.
Sample codes are partial implementaitons of sample playlist and related classes. Preventing manipulations on the playlist item itself is omitted for simplicity.
Common solutions
There are many solutions to these type of situations, four most common of them are as follows;
1- Expose the playlist items as read-only collection and provide manipulation methods in the playlist class
2- Use internal methods or properties
3- Use cancellable pre-manipulation events
4- Hook into the public operations with a hooking interface
1 - Expose the playlist items as read-only collection and provide manipulation methods in the playlist class
You may expose the playlist items as a read-only collection and provide manipulation methods (add, remove...) in the playlist itself.
Sample implementation for read-only item collection and public manipulation methods is as follows;
public class PlaylistWithReadOnlyItems
{
List<PlaylistItem> m_Items;
public IList<PlaylistItem> Items { get; private set; }
public PlaylistWithReadOnlyItems()
{
m_Items = new List<PlaylistItem>();
this.Items = m_Items.AsReadOnly();
}
public void AddItem(PlaylistItem item)
{
m_Items.Add(item);
}
public void RemoveItem(PlaylistItem item)
{
m_Items.Remove(item);
}
}
The disadvantage of this pattern is that, using the Items
property for reading and public methods for manipulating the collection may be confusing for developers. Because developers are accustomed to use exposed collection porperties for both reading and manipulating its items.
Another way of exposing inner items for read-only purposes is providing an indexer which has only get
operator.
public PlaylistItem this[int index]
{
get
{
return m_Items[index];
}
}
2 - Use internal methods or properties
If you are developing a SDK or a component library that will be used by others, managing and protecting the internal state can be accomplished by using internal methods or properties.
The first step is to add internal control methods or properties to the class that will be under control. This sample uses internal properties in order to allow or prevent item operations on the custom collection class.
public class PlaylistWithInternalControlProperties : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
internal bool AllowAdd { get; set; }
internal bool AllowRemove { get; set; }
public void Add(PlaylistItem item)
{
if (this.AllowAdd)
m_Items.Add(item);
else
{
}
}
public bool Remove(PlaylistItem item)
{
if (this.AllowRemove)
return m_Items.Remove(item);
return false;
}
}
The next step is to control item operations from the owner object by settings the internal properties of the controlled object based on the current state. For example, in the below sample, adding and removing items are not allowed if the player is playing the list currently.
public class Player
{
PlaylistWithInternalControlProperties m_playlist;
PlayerState m_State;
public PlayerState State
{
get { return m_State; }
set
{
m_State = value;
m_playlist.AllowAdd = m_State != PlayerState.Playing;
m_playlist.AllowRemove = m_State != PlayerState.Playing;
}
}
}
3 - Use cancellable pre-manipulation events
This pattern is very common in .Net Framework and 3rd party class libraries. This pattern makes use of events for notfying the owner about the changes that is going to be done on an object and give a chnace to cancel the operation.
The first step is to define operation types (mostly an enum definition) and an event args class that contains the information about the operation that is going to be done and has a Cancel
property. System.ComponentModel.CancelEventArgs
class can be used as the base class for the new event args class.
Sample operations enum and event args class are as follows;
public enum ItemOperation
{
Add,
Insert,
Remove,
Clear
}
public class PlaylistItemEventArgs : CancelEventArgs
{
public PlaylistItem Item { get; private set; }
public ItemOperation Operation { get; private set; }
public PlaylistItemEventArgs(PlaylistItem item, ItemOperation operation)
{
this.Item = item;
this.Operation = operation;
}
}
public delegate void PlaylistItemEventHandler(object sender, PlaylistItemEventArgs e);
Next step is to add pre-manipulation events (like ItemAdding
, ItemRemoving
, SomePropertyChanging
) and fire them before the operation is performed.
Third step is to cancel the operation if the Cancel
property of the event args class is set to true
after event processing is finished. There are two options for firing the pre-manipulation event and checking its Cancel
property.
The first option is to call all registered event handlers and check the Cancel
property at the end. OnItemAdding
method in the below code uses this technic. This technic is the most common one in .Net Framework class libraries. The danger of this technic is that, because the event is public and everyone can register to it, one of the methods in event handler chain may clear the Cancel
property to false
and cause the operation be done instead of being cancelled.
Second option is to call each handler, one by one and cancel the operation if one of them sets the Cancel
property to true. OnItemRemoving
method uses this technic.
public class PlaylistItemsCollectionWithCancellableEvents : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
public event PlaylistItemEventHandler ItemAdding;
public event PlaylistItemEventHandler ItemRemoving;
public PlaylistItemsCollectionWithCancellableEvents()
{
m_Items = new List<PlaylistItem>();
}
public void Add(PlaylistItem item)
{
PlaylistItemEventArgs _args = new PlaylistItemEventArgs(item, ItemOperation.Add);
OnItemAdding(_args);
if (_args.Cancel)
return;
m_Items.Add(item);
}
private void OnItemAdding(PlaylistItemEventArgs e)
{
if (ItemAdding != null)
ItemAdding(this, e);
}
public bool Remove(PlaylistItem item)
{
PlaylistItemEventArgs _args = new PlaylistItemEventArgs(item, ItemOperation.Remove);
OnItemAdding(_args);
if (_args.Cancel)
return false;
m_Items.Remove(item);
return true;
}
private void OnItemRemoving(PlaylistItemEventArgs e)
{
Delegate[] _handlers = ItemRemoving.GetInvocationList();
foreach (PlaylistItemEventHandler handler in _handlers)
{
handler(this, e);
if (e.Cancel)
return;
}
}
}
The last step is using these events to control item manipulations in the playlist item collection. The sample player class creates item collection and registers to its pre-manipulation events. When an event occures, checks its internal state and cancels the operation if the operation is not allowed at this state.
public class Player
{
public Player()
{
m_playlist = new PlaylistItemsCollectionWithCancellableEvents();
m_playlist.ItemAdding += new PlaylistItemEventHandler(m_playlist_ItemAdding);
m_playlist.ItemRemoving += new PlaylistItemEventHandler(_playlist_ItemRemoving);
}
private void m_playlist_ItemAdding(object sender, PlaylistItemEventArgs e)
{
if (this.State == PlayerState.Playing)
e.Cancel = true;
}
}
4 - Hook into the public operations with a hooking interface
This technic resembles the observable-observer pattern in Java which does not have simple event mechanism like in C#. Although this technic is very similar to using events, it may be faster and safer for some scenarios. One may also use this technic in order not to pollute the class interface with many pre-manipulation events.
The first step of this pattern is creating an interface for the operations that will be controlled. In this sample, we want to hook into AddItem
and RemoveItem
operations.
public interface IHookingInterface
{
bool CanAddItem(PlaylistItem item);
bool CanRemoveItem(PlaylistItem item);
}
Second step is preparing the controlled class (PlaylistItem
collection in this case) which takes an object that implements the hooking interface. Controlled class (PlaylistWithHookablePublicOperations
) calls the appropriate methods of the hook interafece before it performs a public operation.
In the below example, m_Hook.CanAddItem
is called before performing the real add operation. If the call to m_Hook.CanAddItem
returns true
, the item is added. If it returns false
, it simply does nothing, but sometimes returning false
or throwing an exception may be required to tell to caller that the operation was not performed.
public class PlaylistWithHookablePublicOperations : IList<PlaylistItem>
{
List<PlaylistItem> m_Items;
IHookingInterface m_Hook;
public PlaylistWithHookablePublicOperations(IHookingInterface hook)
{
m_Hook = hook;
m_Items = new List<PlaylistItem>();
}
public void Add(PlaylistItem item)
{
if (m_Hook.CanAddItem(item))
m_Items.Add(item);
else
{
}
}
public bool Remove(PlaylistItem item)
{
if (m_Hook.CanRemoveItem(item))
return m_Items.Remove(item);
return false;
}
}
The next step is preparing the hook class that implements the hooking interface. These classes are mostly private classes defined in the owner classes. This way, it is possible for them to interact with the owner privately (can access private methods and state).
Do not implement this interface at the owner class itself, always create another small class. Implementing hooking interfaces at the owner classes will pollute their interfaces.
class InnerOrPublicHookClass : IHookingInterface
{
Player m_Owner;
public InnerOrPublicHookClass(Player owner)
{
m_Owner = owner;
}
#region IHookingInterface Members
public bool CanAddItem(PlaylistItem item)
{
if (m_Owner.State == PlayerState.Playing)
return false;
return true;
}
public bool CanRemoveItem(PlaylistItem item)
{
if (m_Owner.State == PlayerState.Playing)
return false;
return true;
}
#endregion
}
The last step is passing the hooking obkject to controlled object (the custom, controllable playlist item collection), mostly as a constructor parameter.
public class Player
{
PlaylistWithHookablePublicOperations m_Playlist;
public Player()
{
m_Playlist = new PlaylistWithHookablePublicOperations(new InnerOrPublicHookClass(this));
}
public PlayerState State { get; set; }
}
Although this technic requires a bit more code to write, it is more flexible and perfectly protects the controlled class from unwanted manipulations.
History
- 16 May 2017 - Initial version.
- 22 May 2017 - Read-only indexer option is added to the first method "Expose the playlist items as read-only collection and provide manipulation methods in the playlist class". Thanks to George Swan.
Experienced senior C# developer, sometimes codes in Java and C++ also. He designs and codes desktop applications (WinForms, WPF), windows services and Web APIs (WCF, ASP.Net MVC). He is currently improving his ASP.Net MVC and JavaScript capabilities. He acts as an architect, coder and trainer.
He is mostly experienced in Media Asset Management (MAM) applications, broadcasting sector and medical applications.
LinkedIn: www.linkedin.com/mustafa-kok/
GitHub: github.com/nthdeveloper
StackOverflow: stackoverflow.com/users/1844220/nthdeveloper