Observable properties

From Unify Community Wiki
Jump to: navigation, search


Author: Lonewolfwilliams (http://www.lonewolfwilliams.com)

Contents

Observable Properties

Sometimes when we need data it isn't available. A common example may be when the data is retrieved asynchronously, that is to say that you request the data, wait an unspecified time and then receive the data, typically in a callback.

Observable properties provide an elegant solution to the kind of problem outlined above, essentially they are an event dispatcher wrapped in the accessor method of a class property, that will allow the observing code to 'know' when a property is updated. As we will see the concept can be taken further to make some pretty funky designs.

Why Observe Properties ?

First a simple example, let us suppose that the player username is retrieved from a webservice, asynchronously by the UserDataModel class:

 
class UserDataModel
{
   public event EventHandler UsernameUpdated;
 
   string m_username = null;
   public string UserName
   {
      get
      {
         return m_username;
      }
      set
      {
         var previousValue = m_username;
 
         //we don't want to dispatch an update if the value is unchanged
         if(previousValue == value)
         {
            return;
         }
 
         //you may also want to trap null values
         //if(value == null)
         //{
         //   return;
         //}
 
         m_username = value;
 
         //remember events are essentially function pointers, so they can have a null value if a 
         //no listeners have been registered.
         if(UsernameUpdated != null)
         {
           UsernameUpdated(this, EventArgs.Empty);//you could also make a custom handler, and pass the new value for convenience
         }
      }
   }
 
   public PlayerDataModel()
   {
      //call a webservice and then update UserName when the request returns (remember to use the public property, so that it
      //triggers the event!)
   }
}

Usage

Usage is simple:

string username;
//uh-oh, username is not ready yet...
if(userDataModelInstance.userName == null)
{
   //wait until the data is ready...
   userDataModelInstance.UsernameUpdated += () => username = userDataModelInstance.userName;
}
else 
{
   //or populate immediately
   username = userDataModelInstance.userName;
}

There are some pretty neat things that you can do with this simple pattern:

You could use a co-routine for a non-blocking subroutine that populates something 'in the background' (although, of course, this isn't actually a thread)

IEnumerator populatePlayerHUD()
{
   string username = null;
   if (userDataModelInstance.userName == null)
   {
      userDataModelInstance.UsernameUpdated += () => username = userDataModelInstance.userName;
   }
   else 
   {
      username = userDataModelInstance.userName;
   }
 
   //'spin' whilst we wait for data...
   while(username == null)
   {
      yield return 1;
   }
 
   //continue populating the hud with other player data
}

Or you could add observable properties to a datamodel that has some synchronous and some asynchronous data, that way (with some clever design decisions) you don't have to halt execution until the whole model is available, just pause the functionality specific to the asynchronous data.

public class 
{
   PlayerDataModel m_player;
   bool m_playerAsynchDataPopulated = false;
 
   void onUpdate()
   {
      m_player.score += 10;
 
      //this will fail if the playerdatamodel isn't ready, which does leave a small chance
      //that the player highscore may not be captured (the player dies before the high score is 
      //retrieved) but that is the trade-off for not making the player wait (plus you could always
      //wait on game over to ensure that the high score got written)
      if( m_playerAsynchDataPopulated &&
          m_player.score > m_player.highScore)
      {
          m_player.highScore = m_player.score;
      }
   }   
 
   void InitialiseGame()
   {
      m_player = new PlayerDataModel(); //could be served from a factory class...
 
      initialiseSynchronousPlayerData(); //these are the default values of the class...
      StartCoroutine(initialiseAsynchronousPlayerData()); //these come from an api call...
   }
 
   void initialiseSynchronousPlayerData()
   {
      DisplayPlayerLivesOnScreen(newPlayer.lives);
   }
 
   IEnumerator initialiseAsynchronousPlayerData()
   {
      string username = null;
      if (userDataModelInstance.userName == null)
      {
         userDataModelInstance.UsernameUpdated += () => username = userDataModelInstance.userName;
      }
      else 
      {
         username = userDataModelInstance.userName;
      }
 
      //'spin' whilst we wait for data...
      while(username == null)
      {
         yield return 1;
      }
 
      DisplayPlayerNameOnScreen(username);
 
      int highScore = -1;
      if (userDataModelInstance.highScore == -1)
      {
         userDataModelInstance.HighScoreUpdated += () => highScore = userDataModelInstance.highScore;
      }
      else 
      {
         highScore = userDataModelInstance.highScore;
      }
 
      //'spin' whilst we wait for data...
      while(username == null)
      {
         yield return 1;
      }
 
      DisplayPlayerHighScoreOnScreen(highScore);
 
      m_playerAsynchDataPopulated = true;
   }
}

An Observable List

If you want to make your code more modular, you could make observable versions of types that you use often, for example here is an implementation of an observable list that I use (for those who are wondering why I have written a wrapper class, instead of subclassing, I prefer composition over inheritance as a design choice):

 
using System;
using System.Collections;
using System.Collections.Generic;
 
/*
 * Lonewolfwilliams (http://www.lonewolfwilliams.com)
 * 
 * a list that dispatches events when it is updated
 */
 
namespace com.lonewolfwilliams.utility
{
   public class ObservableList<T> : IList<T>
   {	
      public delegate void ListUpdateHandler(object sender, object updatedValue);
      public event ListUpdateHandler ItemAdded;	
      public event ListUpdateHandler ItemRemoved;	
      public event EventHandler ListCleared;
      List<T> m_list = new List<T>();
 
      #region IList[T] implementation
      public int IndexOf (T value)
      {
         return m_list.IndexOf(value);
      }
 
      public void Insert (int index, T value)
      {
         m_list.Insert(index, value);
      }
 
      public void RemoveAt(int index)
      {
         m_list.RemoveAt(index);
      }
 
      public T this[int index] 
      {
         get 
         {
            return m_list[index];
         }
         set 
         {
            m_list[index] = value;
         }
      }
      #endregion
 
      #region IEnumerable implementation
      public IEnumerator<T> GetEnumerator ()
      {
         return m_list.GetEnumerator();
      }
 
      IEnumerator IEnumerable.GetEnumerator ()
      {
         return GetEnumerator();
      }
      #endregion
 
      #region ICollection[T] implementation
      public void Add (T item)
      {
         if(ItemAdded != null)
         {
            ItemAdded(this, item);	
         }
 
         m_list.Add(item);
      }
 
      public void Clear()
      {
         m_list.Clear();	
 
         if(ListCleared != null)
         {
            ListCleared(this, EventArgs.Empty);	
         }
      }
 
      public bool Contains (T item)
      {
         return m_list.Contains(item);
      }
 
      public void CopyTo (T[] array, int arrayIndex)
      {
         m_list.CopyTo(array, arrayIndex);
      }
 
      public bool Remove (T item)
      {
         if(ItemRemoved != null)
         {
            ItemRemoved(this, item);	
         }
 
         return m_list.Remove(item);
      }
 
      public int Count
      {
         get 
         { 
            return m_list.Count;
         }
      }
 
      public bool IsReadOnly
      {
         get 
         { 
            return false;
         }
      }
      #endregion
   }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox