четверг, 25 июля 2013 г.

Сохранение настроек приложения в изолированном хранилище

По поводу хранения настроек приложения написано уже очень много статей. Но о том, что сохранять свои данные можно так же в изолированном хранилище - очень мало упоминаний. Я не буду рассказывать о том, что это такое (об этом очень хорошо написано на MSDN), я просто хочу предложить некоторое решение, которое позволит вам хранить свои данные без особых усилий.

Что из себя представляют настройки приложения? Это коллекция пар ключ/значение. То есть это некоторый IDictionary<TKey, TValue>, где в качестве ключа выступает, как правило, строковый параметр, а в качестве значения, в идеале, может быть любой объект. Если внимательно посмотреть на пространство имен System.IO.IsolatedStorage, то можно обнаружить, что класс с таким функционалом уже есть - это IsolatedStorageSettings. Однако он доступен только для Silverlight и Silverlight for Windows Phone. А для настольных приложений доступен только класс IsolatedStorageFile, который представляет область изолированного хранения, содержащую файлы и папки. То есть разработчики настольных приложений здесь несколько обделены функционалом. Собственно этот пробел я и решил заполнить.

Идея до безобразия проста: ключ - это файл в изолированном хранилище, в котором хранится значение этого ключа в виде сериализованных двоичных данных. Однако, если ключ будет состоять из символов, которые не могут содержаться в имени файла (\/:*?"<>|), возникает проблема. Решение, в принципе, такое же простое - хранить значения оригинальных ключей в отдельном служебном файле и каждому такому значению сопоставлять уникальный идентификатор, по имени которого будет создаваться файл для хранения значения оригинального ключа. Если не совсем понятно, то надеюсь рисунок все прояснит:

(info.dat - служебный файл, Key - оригинальный ключ, Guid - уникальный идентификатор)


На этом теория заканчивается, приступаем к практике. Для начала создадим вспомогательный класс для сохранения объектов в изолированном хранилище в виде двоичных данных:

IsolatedStorageSerializer.cs:
/// <summary>
/// Сериализует и десериализует объекты в двоичный формат.
/// </summary>
internal static class IsolatedStorageSerializer
{
    #region Public Methods

    /// <summary>
    /// Сериализует указанный объект в файл.
    /// </summary>
    /// <param name="instance">Объект для сериализации.</param>
    /// <param name="path">Путь к файлу.</param>
    /// <param name="storageFile">Область изолированного хранения, содержащая файл.</param>
    /// <returns>True - объект сериализован, иначе - False.</returns>
    public static Boolean BinarySerialize(Object instance, String path, IsolatedStorageFile storageFile)
    {
        // Проверка объектов
        if (string.IsNullOrEmpty(path)) return false;
        var result = false;
        // Подготовка потоков
        using (var fileStream = new IsolatedStorageFileStream(path, FileMode.OpenOrCreate, storageFile))
        {
            // Десериализуем объект
            var binaryFormatter = new BinaryFormatter();
            try
            {
                binaryFormatter.Serialize(fileStream, instance);
                result = true;
            }
            catch (Exception ex)
            {
                // Возникла ошибка при десериализации
                Debug.WriteLine(ex);
            }
        }
        return result;
    }

    /// <summary>
    /// Десериализует указанный сериализованный ранее объект.
    /// </summary>
    /// <param name="path">Путь к файлу.</param>
    /// <param name="storageFile">Область изолированного хранения, содержащая файл.</param>
    /// <returns>Десериализованный объект или null в случае неудачи.</returns>
    public static Object BinaryDeserialize(String path, IsolatedStorageFile storageFile)
    {
        // Проверка объектов
        if (string.IsNullOrEmpty(path)) return null;
        Object instance = null;
        // Подготовка потоков
        using (var fileStream = new IsolatedStorageFileStream(path, FileMode.OpenOrCreate, storageFile))
        {
            // Десериализуем объект
            var binaryFormatter = new BinaryFormatter();
            try
            {
                instance = binaryFormatter.Deserialize(fileStream);
            }
            catch (Exception ex)
            {
                // Возникла ошибка при десериализации
                Debug.WriteLine(ex);
            }
        }
        return instance;
    }

    #endregion

    #region Helper Members

    /// <summary>
    /// Определяет, возможно ли сериализовать объект.
    /// </summary>
    /// <param name="value">Объект, который требуется сериализовать.</param>
    /// <returns>true - объект сериализуем, иначе - false.</returns>
    public static Boolean IsSerializable(this Object value)
    {
        if (value is ISerializable || value.GetType().IsSerializable) return true;
        return Attribute.IsDefined(value.GetType(), typeof(SerializableAttribute));
    }

    #endregion
}

Также в этом классе есть метод, который проверяет, возможно ли сериализовать объект или нет. Теперь можно реализовать собственный класс IsolatedStorageSettings (который по своему интерфейсу похож на класс, который доступен в Silverlight):

IsolatedStorageSettings.cs:
/// <summary>
/// Provides a dictionary that stores key-value pairs in isolated storage.
/// </summary>
public sealed class IsolatedStorageSettings : IEnumerable, INotifyCollectionChanged
{
    #region Fields

    private const String SettingsFile = "info.dat";
    private readonly IDictionary<String, KeyValuePair<Guid, Object>> dictionary;
    private readonly IsolatedStorageFile storageFile;

    #endregion

    #region Ctor

    /// <summary>
    /// Initializes a new instance of the IsolatedStorageSettings class.
    /// </summary>
    static IsolatedStorageSettings()
    {
        if (ApplicationSettings == null) ApplicationSettings = new IsolatedStorageSettings();
    }

    /// <summary>
    /// Initializes a new instance of the IsolatedStorageSettings class.
    /// </summary>
    private IsolatedStorageSettings()
    {
        dictionary = new Dictionary<String, KeyValuePair<Guid, Object>>();
        storageFile = IsolatedStorageFile.GetUserStoreForAssembly();
        Restore();
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets an instance of IsolatedStorageSettings.
    /// </summary>
    public static IsolatedStorageSettings ApplicationSettings { get; private set; }

    /// <summary>
    /// Gets the number of key-value pairs that are stored in the dictionary.
    /// </summary>
    public Int32 Count
    {
        get { return dictionary.Count; }
    }

    /// <summary>
    /// Gets the value associated with the specified key.
    /// </summary>
    /// <param name="key">The key of the item to get.</param>
    /// <returns>The value associated with the specified key.</returns>
    public Object this[String key]
    {
        get
        {
            KeyValuePair<Guid, Object> keyValuePair;
            return dictionary.TryGetValue(key, out keyValuePair) ? keyValuePair.Value : null;
        }
    }

    /// <summary>
    /// Gets a collection that contains the keys in the dictionary.
    /// </summary>
    public IEnumerable<String> Keys
    {
        get { return dictionary.Keys; }
    }

    /// <summary>
    /// Gets a collection that contains the values in the dictionary.
    /// </summary>
    public IEnumerable<Object> Values
    {
        get { return dictionary.Values.Select(keyValuePair => keyValuePair.Value); }
    }

    #endregion
}

Как вы можете заметить, словарь IDictionary<String, KeyValuePair<Guid, Object>> dictionary хранит оригинальный ключ и пару уникального идентификатора вместе со значением ключа. Данный класс запечатанный и имеет статическое свойство, которое возвращает его экземпляр (это сделано для того, чтобы иметь один экземпляр на все приложение). Стандартные свойства я описывать не буду, а приведу содержание метода Restore(), который восстанавливает ранее сохраненные значения:

IsolatedStorageSettings.cs:
/// <summary>
/// Restore settings.
/// </summary>
private void Restore()
{
    var settings = IsolatedStorageSerializer.BinaryDeserialize(SettingsFile, storageFile) as Dictionary<String, Guid> ?? new Dictionary<String, Guid>();
    foreach (var item in settings)
    {
        var value = IsolatedStorageSerializer.BinaryDeserialize(item.Value.ToString(), storageFile);
        dictionary.Add(item.Key, new KeyValuePair<Guid, Object>(item.Value, value));
    }
}

Сначала мы получаем ключи и соответствующие им уникальные идентификаторы из служебного файла, затем по идентификаторам восстанавливаем значения ключей из файлов и сохраняем в словарь.

Остается реализовать стандартные операции (CUD) для этого класса. Приведу код на примере создания нового ключа в настройках (весь код можно посмотреть в исходниках на GitHub):

IsolatedStorageSettings.cs:
/// <summary>
/// Adds an entry to the dictionary for the key-value pair.
/// </summary>
/// <param name="key">The key for the entry to be stored.</param>
/// <param name="value">The value to be stored.</param>
/// <returns>true if the specified key was added; otherwise, false.</returns>
public Boolean TryAdd(String key, Object value)
{
    if (key == null) throw new ArgumentNullException("key");
    if (value == null) throw new ArgumentNullException("value");
    if (key.Equals(SettingsFile)) throw new ArgumentException("key");
    if (!value.IsSerializable()) throw new SerializationException("A object could not be serialized.");
    lock (syncLock)
    {
        Boolean result;
        if (dictionary.ContainsKey(key))
            result = TryUpdateWithNotification(key, value);
        else
        {
            result = TryAddWithNotification(key, value);
            if (result) Save();
        }
        return result;
    }
}

/// <summary>Attempts to add an item to the dictionary, notifying observers of any changes.</summary>
/// <param name="key">The key of the item to be added.</param>
/// <param name="value">The value of the item to be added.</param>
/// <returns>Whether the add was successful.</returns>
private Boolean TryAddWithNotification(String key, Object value)
{
    var guid = Guid.NewGuid();
    var result = IsolatedStorageSerializer.BinarySerialize(value, guid.ToString(), storageFile);
    if (result)
    {
        dictionary.Add(key, new KeyValuePair<Guid, Object>(guid, value));
        RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,
                                                                    new KeyValuePair<String, Object>(key, value)));
    }
    return result;
}

/// <summary>Attempts to update an item in the dictionary, notifying observers of any changes.</summary>
/// <param name="key">The key of the item to be updated.</param>
/// <param name="value">The new value to set for the item.</param>
/// <returns>Whether the update was successful.</returns>
private Boolean TryUpdateWithNotification(String key, Object value)
{
    if (!value.IsSerializable()) throw new SerializationException("A object could not be serialized.");
    KeyValuePair<Guid, Object> oldPair;
    var result = false;
    if (dictionary.TryGetValue(key, out oldPair))
    {
        result = IsolatedStorageSerializer.BinarySerialize(value, oldPair.Key.ToString(), storageFile);
        var newPair = new KeyValuePair<Guid, Object>(oldPair.Key, value);
        dictionary[key] = newPair;
        RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
                                                                    new KeyValuePair<String, Object>(key, newPair.Value),
                                                                    new KeyValuePair<String, Object>(key, oldPair.Value)));
    }
    return result;
}

Открытый метод TryAdd() вначале проверяет входные параметры, затем, если ключ с таким значением уже существует - обновляет его, иначе - создает новый. Закрытые методы TryAddWithNotification() и TryUpdateWithNotification() выполняют всю работу по сохранению значений и сообщают об изменении коллекции. Использовать данный класс достаточно удобно:

Program.cs:
static void Main(string[] args)
{
    var settings = IsolatedStorageSettings.ApplicationSettings;

    settings.TryAdd("String key", "Hello world!!!");
    settings.TryAdd("Int key", Int32.MaxValue);
    settings.TryAdd("Object key", TimeZoneInfo.GetSystemTimeZones());

    object value;
    settings.TryGet("Int key", out value);
    System.Console.WriteLine(value);
    settings.TryRemove("Int key");

    System.Console.ReadKey();
}

Данный класс я реализовал как потокобезопасный, таким образом можно сохранять настройки приложения из различных потоков. Есть замечания или есть что добавить? С удовольствием прочитаю ваши комментарии...

Исходный код

Комментариев нет:

Отправить комментарий