Using Isolated Storage in Silverlight with a Generic Repository

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 IUnityContainer _unityContainer;
        private readonly CartIsolatedStorageRepository _cartRepository;

        public SampleViewModel(IUnityContainer unityContainer, CartIsolatedStorageRepository cartRepository)
        {
            _unityContainer = unityContainer;
            _cartRepository = cartRepository;

            Cart = new ObservableCollection<AssetViewModel>();

            foreach (var asset in _cartRepository.SelectAll())
            {
                var assetviewmodel = _unityContainer.Resolve<AssetViewModel>();
                assetviewmodel.Load(asset);
                Cart.Add(assetviewmodel);
            }

            Cart.CollectionChanged += Cart_CollectionChanged;

            ProcessCartCommand = new DelegateCommand<object>(ProcessOrderCommandExecute);
            AddAssetCommand = new DelegateCommand<object>(AddAssetCommandExecute, AddAssetCommandCanExecute);
            DeleteAssetCommand = new DelegateCommand<object>(DeleteItemCommandExecute);
        }
        
        void Cart_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (AssetViewModel newItem in e.NewItems)
                    {
                        _cartRepository.Insert(newItem.Asset);
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (AssetViewModel oldItem in e.OldItems)
                    {
                        _cartRepository.Delete(oldItem.Asset);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

        }

        public ObservableCollection<AssetViewModel> Cart
        {
            get; 
            private set;
        }
	

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

 
	Cart = new ObservableCollection<AssetViewModel>();
	
This has NotifyCollectionChangedAction.Replace, that was called when the assignment by index was made:
 
	Cart[0] = _unityContainer.Resolve<AssetViewModel>();
	
This will never occur in the setup of our ViewModel in either case. This is 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 CartIsolatedStorageRepository : TIsolatedStorageRepository<Asset>
		{
			public CartIsolatedStorageRepository() 
				: base("Cart.xml")
			{
			}

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

Extending

Our current view does not allow record editing. This is why the generic repository does not contain an update. To implement an update, the developer would need to subscribe to the PropertyChanged event of the ViewModel being persisted, Asset, and ensure 'Equals' compare the primary key solely for equality.

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

 
		 #if SILVERLIGHT
			public class Asset : INotifyPropertyChanged, IEquatable<Asset>
			{
				private string _barcode = String.Empty;
				public string Barcode
				{
					get { return _barcode; }
					set { _barcode = value; RaisePropertyChanged("Barcode"); }
				}

				private int _quantity;
				public int Quantity
				{
					get { return _quantity; }
					set { _quantity = value; RaisePropertyChanged("Quantity"); }
				}


				public event PropertyChangedEventHandler PropertyChanged;
				private void RaisePropertyChanged(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 (Asset)) return false;
					return Equals((Asset) obj);
				}

				public bool Equals(Asset other)
				{
					if (ReferenceEquals(null, other)) return false;
					if (ReferenceEquals(this, other)) return true;
					return Equals(other._barcode, _barcode) && other._quantity == _quantity;
				}

				public override int GetHashCode()
				{
					unchecked
					{
						return ((_barcode != null ? _barcode.GetHashCode() : 0)*397) ^ _quantity;
					}
				}

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

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

Things to be aware of

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

Please note that these methods in generic repository create exceptions. This is deliberate, as Isolated Storage is not guaranteed and can throw exceptions based on user specific quota limits.

While Isolated Storage is nice to have, it should not cause breaks in my application. I these methods are functionally flawed, then we might do better avoiding them.

References

 
Author: Leblanc Meneses
 
Gravatar

Leblanc has years of industry experience in all aspects of the software development life cycle. He has gathered and managed requirements, and has demonstrated his object-oriented architecture design with many UML and ER diagrams. He has also established his implementation abilities using a test-driven development approach, utilizing various languages and platforms. He has implemented continuous integration tools to maintain and integrate developed products.

He is currently a Silverlight/WPF/ASP.NET MVC/WCF Consultant in Dallas, Texas, and always welcomes inquiries from potential clients in Fort Worth, Dallas, Irving, Richardson, and Plano.

twitter @leblancmeneses linkedin

5 Comments

  • Gravatar Reply

    Gareth Watt

    # June 1, 2010 - 5:07 AM

     
    Leblanc

    Excellent article. Thanks for sharing this with the community :)

    Gareth

    www.moneydashboard.com
    • Gravatar Reply

      Leblanc Meneses

      # July 7, 2010 - 4:47 AM

       
      thanks. By the way congrats on the bizspark spotlight several weeks ago (I remember seeing moneydashboard).
  • Gravatar Reply

    Michael Washington

    # June 1, 2010 - 5:43 AM

     
    Excellent. This is a very big deal. I plan to use this.

    This sort of functionality is WHY a app should use Silverlight. It is a better experience for the end user when the app is able to persist important information

    If people have a hard time understanding this, think "big cookie". However, it is a big "type safe" cookie...
    • Gravatar Reply

      Leblanc Meneses

      # July 7, 2010 - 4:47 AM

       
      thanks. Not to mention the developer experience and productiveness is the best anywhere.
  • Gravatar Reply

    Leblanc Meneses

    # July 9, 2010 - 4:34 PM

     
    I recently seen some 'document databases' that would work well in Silverlight and WP7 (Windows Phone 7). They use Isolated Storage as its storage facility.

    An example would be <a href='http://sterling.codeplex.com/documentation'>Sterling OODB for Silverlight and Windows Phone 7</a>

    Whilst this article shows you how you can role your own fairly quickly, a document database is ideal in cases where a lot of objects need to be persisted and queried.
X

Leave a Response

All comments on this article are moderated