Using Isolated Storage in Silverlight with a Generic Repository

Posted: Saturday, May 29, 2010   # comments   


Isolated storage is a file system. It is "isolated" because it is rooted to a specific directory, which provides the sandbox needed for browser security.

Get a pointer to the file system

There are two types of file systems in Silverlight:

  1. IsolatedStorageFile.GetUserStoreForApplication() - This provides a file system specific to an application. From a development standpoint, you can consider the file system unique to each XAP.
  2. IsolatedStorageFile.GetUserStoreForSite() - This provides a file system specific to the site. This could be used to share data between XAPs on the same site.

Both of these methods provide the file system in a class called IsolatedStorageFile. This class has the same API that you find in System.IO File and Directory classes for desktop/server applications.

 

Generic repository for persisting the ViewModel

Creating a base generic repository allowed me to centralize common IsolatedStorageFile operations. These could be needed any time. I wanted to save, retrieve, or delete any type of object from Isolated Storage in my application, while still being strongly typed.

 

		public abstract class TIsolatedStorageRepository<T>
		{
			private readonly string _filename;
			
			protected TIsolatedStorageRepository(string filename)
			{
				_filename = filename;
			}

			protected abstract IsolatedStorageFile GetUserStore();


			public IEnumerable<T> SelectAll()
			{
				var items = new List<T>();
				try
				{
					using (var filesystem = GetUserStore())
					using (var filestream = new IsolatedStorageFileStream(_filename, FileMode.OpenOrCreate, filesystem))
					{
						if (filestream.Length == 0)
							return items;

						var transform = new XmlSerializer(typeof (List<T>));
						items = (List<T>) transform.Deserialize(filestream);
					}
				}
				catch
				{
				}

				return items;
			}

			public void Insert(T item)
			{
				try
				{
					var items = new List<T>();
					using (var filesystem = GetUserStore())
					using (var filestream = new IsolatedStorageFileStream(_filename, FileMode.OpenOrCreate, filesystem))
					{
						var transform = new XmlSerializer(typeof (List<T>));

						if (filestream.Length != 0)
						{
							items = (List<T>) transform.Deserialize(filestream);
						}

						filestream.Flush();
						filestream.SetLength(0);
						filestream.Seek(0, SeekOrigin.Begin);

						items.Add(item);
						transform.Serialize(filestream, items);
					}
				}
				catch
				{
				}
			}

			public void Delete(T item)
			{
				try
				{
					var items = new List<T>();
					using (var filesystem = GetUserStore())
					using (var filestream = new IsolatedStorageFileStream(_filename, FileMode.OpenOrCreate, filesystem))
					{
						var transform = new XmlSerializer(typeof (List<T>));

						if (filestream.Length != 0)
						{
							items = (List<T>) transform.Deserialize(filestream);
						}

						var matches = items.Where(i => i.Equals(item));

						var keep = items.Except(matches).ToList();

						filestream.Flush();
						filestream.SetLength(0);
						filestream.Seek(0, SeekOrigin.Begin);
						transform.Serialize(filestream, keep);
					}
				}
				catch
				{
				}
			}
		}

How do I use it?

Following a view first approach when designing with MVVM. Lets review the goal.

isolated storage use case

In this UI, the user scans items into a shopping cart, which requires the user to explicitly call 'Process Order' when the batch is ready to be processed. The user can accidentally close the browser at any time, or the browser could freeze. Several minutes of manually scanning could be lost this way.

So the goal is to have the 'Cart' populated from Isolated Storage on initial load. As records are added and removed we have to keep the 'Cart' collection in sync with the collection represented in Isolated Storage.

Here is the excerpt from the viewmodel that we need to implement this interaction (Using PRISM DelegateCommand). The trick is to attach to the CollectionChanged event, which has been exposed by the ObservableCollection.

 

        private readonly CheckInIsolatedPersistentStorageRepository _checkInIsolatedPersistentStorageRepository;
		
	    public CheckInViewModel(
            CheckInIsolatedPersistentStorageRepository checkInIsolatedPersistentStorageRepository)
        {
            _checkInIsolatedPersistentStorageRepository = checkInIsolatedPersistentStorageRepository;
			
            ScannedItems = new ObservableCollection<CheckInAssociation>();
            foreach (CheckInAssociation checkInAssociation in _checkInIsolatedPersistentStorageRepository.SelectAll())
            {
                ScannedItems.Add(checkInAssociation);
            }

            ScannedItems.CollectionChanged += ScannedItems_CollectionChanged;

            CommitCommand = new DelegateCommand<object>(CommitCommandFunc);
            AddCommand = new DelegateCommand<object>(AddCommandFunc, AddCommandCanExecute);
            DeleteCommand = new DelegateCommand<object>(DeleteCommandFunc);
        }

        void ScannedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (CheckInAssociation newItem in e.NewItems)
                    {
                        _checkInIsolatedPersistentStorageRepository.Insert(newItem);
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (CheckInAssociation oldItem in e.OldItems)
                    {
                        _checkInIsolatedPersistentStorageRepository.Delete(oldItem);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        public ObservableCollection<CheckInAssociation> ScannedItems
        {
            get;
            private set;
        }

 

NotifyCollectionChangedEventArgs Action also has NotifyCollectionChangedAction.Reset that is called when the following assignment is made:

 
ScannedItems = new ObservableCollection<checkinassociation>();

and has NotifyCollectionChangedAction.Replace that is called when this assignment by index is made:

 
	ScannedItems[0] = new CheckInAssociation();
	

In both cases this will never occur in the setup of our ViewModel and why we consider throwing an ArgumentOutOfRangeException as acceptable.

 

The benefit of using the generic repository can be seen by the concrete Isolated Storage class we created for this view.

 

 
		public class CheckInIsolatedPersistentStorageRepository : TIsolatedStorageRepository<checkinassociation>
		{
			public CheckInIsolatedPersistentStorageRepository()
				: base("CheckIn.xml")
			{
			}

			protected override IsolatedStorageFile GetUserStore()
			{
				return IsolatedStorageFile.GetUserStoreForApplication();
			}
		}

 

Extending

Our current view does not allow for edit of a record, hence, why the generic repository does not contain an update. To implement update the developer would need to subscribe to PropertyChanged event of the ViewModel being persisted, CheckInAssociation, and ensure 'Equals' compare the primary key only for equality.

Here is how Equals needs to be written with some help from Resharper.

 
		#if SILVERLIGHT
			public class CheckInAssociation : INotifyPropertyChanged, IEquatable<checkinassociation>
			{
				private String _rfidTagGrai;
				public String RfidTagGrai
				{
					get { return _rfidTagGrai; }
					set { _rfidTagGrai = value; OnPropertyChanged("RfidTagGrai"); }
				}

				private String _barCodeTagId;
				public String BarCodeTagId
				{
					get { return _barCodeTagId; }
					set { _barCodeTagId = value; OnPropertyChanged("BarCodeTagId"); }
				}

				private Guid _batchID;
				public Guid BatchID
				{
					get { return _batchID; }
					set { _batchID = value; OnPropertyChanged("BatchID"); }
				}

				public event PropertyChangedEventHandler PropertyChanged;
				private void OnPropertyChanged(String propertyName)
				{
					if(PropertyChanged != null)
						PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
				}






				public override bool Equals(object obj)
				{
					if (ReferenceEquals(null, obj)) return false;
					if (ReferenceEquals(this, obj)) return true;
					if (obj.GetType() != typeof (CheckInAssociation)) return false;
					return Equals((CheckInAssociation) obj);
				}

				public bool Equals(CheckInAssociation other)
				{
					if (ReferenceEquals(null, other)) return false;
					if (ReferenceEquals(this, other)) return true;
					return Equals(other._rfidTagGrai, _rfidTagGrai) && Equals(other._barCodeTagId, _barCodeTagId) && other._batchID.Equals(_batchID);
				}

				public override int GetHashCode()
				{
					unchecked
					{
						int result = (_rfidTagGrai != null ? _rfidTagGrai.GetHashCode() : 0);
						result = (result*397) ^ (_barCodeTagId != null ? _barCodeTagId.GetHashCode() : 0);
						result = (result*397) ^ _batchID.GetHashCode();
						return result;
					}
				}

				public static bool operator ==(CheckInAssociation left, CheckInAssociation right)
				{
					return Equals(left, right);
				}

				public static bool operator !=(CheckInAssociation left, CheckInAssociation right)
				{
					return !Equals(left, right);
				}
			}
		#else

 

Thing to be aware of

This repository is non transactional. Setting filestream.SetLength(0); will be committed but an exception might occur during Serialize process therefore deleting previous records.

Make note that these methods in generic repository eat exceptions. This is done on purpose because Isolated Storage is not guaranteed and can throw exceptions based on quota limits that is user specific.

While Isolated Storage is nice to have it should not break my application if these methods are having problems functioning.

References

Silverlight, PRISM, Storage,




Need help now?

1 on 1 with  


Available on Skype, Hangouts, or TeamViewer.

Reserve Now $500