Статьи

WP7 – Понимание сериализации

Когда я изучал и писал свое первое приложение WP7, я понял, что понимание механизма сериализации жизненно важно, если вы хотите написать приложение WP7. Вы, вероятно, в конечном итоге будете использовать его при сохранении данных в изолированном хранилище и (неявно) при сохранении состояния приложения в состояние страницы или в глобальный объект состояния приложения.

Поэтому я пишу этот пост в качестве напоминания для себя и для краткого изложения некоторых ключевых моментов по этому вопросу.

WP7 фактически поддерживает различные типы сериализации дерева:

  • Двоичная сериализация — это самая быстрая и самая компактная форма сериализации, но она также менее гибкая.
  • Сериализация XML.
  • Сериализация DataContract — это метод по умолчанию, используемый платформой при сериализации и десериализации объектов в разных словарях состояния приложения.

Я не буду углубляться в каждую их деталь, потому что вокруг уже много документации (особенно в MSDN). Остальная часть поста будет в основном посвящена сериализации DataContract.

  • Вы можете сериализовать открытые типы, поля и свойства; Ваши свойства должны иметь геттер и сеттер, чтобы сериализоваться (вы получите исключение, если попытаетесь сериализовать общедоступное свойство без геттера).
  • DataContractSerializer работает по принципу согласия, что означает, что вам нужно указать, что вы хотите сериализовать, используя атрибуты [DataContract] (для классов) и [DataMember] (для поля и свойств).
  • Если вы не укажете атрибуты [DataContract] и [DataMember] и ваш тип будет публичным, любые общедоступные свойства и поля будут сериализованы.
  • Вы можете «нарушить правило» и сохранить внутренний класс или внутренние члены, если пометите сборки, содержащие ваши типы, с помощью атрибута [InternalsVisibleTo] что-то вроде:
[assembly: InternalsVisibleTo("System.Runtime.Serialization")]

Но это не рекомендуется, и в документации явно сказано, что этот атрибут не должен использоваться непосредственно в нашем коде. Если вы не укажете этот атрибут, вы получите исключение SecurityException при попытке сериализации внутреннего класса.

  • При десериализации объект Constructor вызываться не будет. Поэтому, если в конструкторе есть часть вашей логики инициализации объекта, не ожидайте, что она будет вызвана во время десериализации. Вам нужно будет реорганизовать его в метод и вызвать его после десериализации объекта. Следующий фрагмент кода показывает пример:
[DataContract]
public class EntityWithConstructor
{
	public string M1;

	[DataMember]
	public int P1 { get; set; }

	public EntityWithConstructor()
	{
		M1 = "Intialized";
	}
}

...
// object constructor is not called during deserialization
EntityWithConstructor obj = new EntityWithConstructor();
string data = DataContractSerializerHelpers.Serialize(obj);
MessageBox.Show(data);
EntityWithConstructor deser = DataContractSerializerHelpers.Deserialize<EntityWithConstructor>(data);
// deser.M1 will be "", instead of the "Initialized" specified in the parameterless contructor
MessageBox.Show("Constructor initialized value (it should be 'Initialized': " + deser.M1);
  • По умолчанию ссылки на объекты не сохраняются: если вам нужно сохранить идентичность объекта при сохранении сложных иерархий, вам нужно установить для параметра «IsReference» значение true в объявлении атрибута [DataContract]. Это очень помогает предотвратить проблему циклических ссылок, которая может возникнуть с сшитыми объектами. В качестве примера проверьте следующий код:
[DataContract]
public class Entity
{
	public int M1;

	[DataMember]
	public string P1 { get; set; }
}

[DataContract(IsReference = true)]
public class IsReferenceEntity
{
	public int M1;

	[DataMember]
	public string P1 { get; set; }
}

public class EntityContainer
{
	public List<Entity> Entities;

	public Entity SelectedEntity;

	public List<IsReferenceEntity> IsReferenceEntities;

	public IsReferenceEntity SelectedIsReferenceEntity;
}
...
// references are preserved if you specify the 'IsReference = true' parameter of DataContract attribute
var cont = new EntityContainer();
cont.Entities = new List<Entity> {new Entity() { P1 = "E1"}};
cont.SelectedEntity = cont.Entities[0];
cont.IsReferenceEntities = new List<IsReferenceEntity>() {new IsReferenceEntity(){ P1 = "Ref1"}};
cont.SelectedIsReferenceEntity = cont.IsReferenceEntities[0];

string data = DataContractSerializerHelpers.Serialize(cont);
MessageBox.Show(data);
EntityContainer deser = DataContractSerializerHelpers.Deserialize<EntityContainer>(data);

bool isEqualEntityP1Content = deser.Entities[0].P1 == deser.SelectedEntity.P1; // true
bool isSameEntityInstance = deser.Entities[0] == deser.SelectedEntity; //false

bool isEqualIsReferenceEntityP1Content = deser.IsReferenceEntities[0].P1 == deser.SelectedIsReferenceEntity.P1; //true
bool isSameIsReferenceEntityInstance = deser.IsReferenceEntities[0] == deser.SelectedIsReferenceEntity; //true

string message =
	string.Format(
		"Entity\n - same content: {0}\n - same reference: {1} \n\nIsReferenceEntity\n - same content: {2}\n - same reference: {3}",
		isEqualEntityP1Content, isSameEntityInstance, isEqualIsReferenceEntityP1Content, isSameIsReferenceEntityInstance);
MessageBox.Show(message);
  • DataContractSerializer может сохранять унаследованные иерархии классов, но если у сохраняемых объектов есть члены, указывающие на типы базовых классов, вам нужно будет пометить сохраняемые вами классы атрибутом [KnownTypes], чтобы сообщить DataContractSerializer обо всех типы, вовлеченные в процесс сериализации:
[DataContract]
public class Entity
{
	public int M1;

	[DataMember]
	public string P1 { get; set; }
}

[DataContract]
public class Derived : Entity
{
	[DataMember]
	public string P2 { get; set; }
}

[DataContract]
[KnownType(typeof(Derived))]
public class DerivedContainer
{
	[DataMember]
	public Entity Entity { get; set; }
}
...
DerivedContainer cont = new DerivedContainer();
cont.Entity = new Derived() {P2 = "data"};

string data = DataContractSerializerHelpers.Serialize(cont);
MessageBox.Show(data);
DerivedContainer deser = DataContractSerializerHelpers.Deserialize<DerivedContainer>(data);
// deser.Entity is of type Derived.
MessageBox.Show("Type inside Entity field: " + deser.Entity.GetType().Name);

Эти примеры должны охватывать большинство обычных сценариев использования сериализации контрактов данных. В приложении к этой статье вы можете найти простой проект, который охватывает все иллюстрированные случаи.

Однако я хотел бы добавить еще несколько соображений, относящихся к WP7, очень важно понимать, КОГДА среда WP7 выполняет этапы автоматической сериализации и десериализации и где искать ошибки сериализации.

Когда вы добавляете объект в один из двух словарей состояний (будь то объект в PhoneApplicationPage или объект в PhoneApplicationServices), он не сериализуется немедленно; фактическая операция откладывается до тех пор, пока вы не уйдете со страницы или ваше приложение не будет «захоронено».

Аналогичным образом десериализация происходит при переходе на страницу (для словаря Page.State) и при повторной активации приложения после «захоронения».

Из-за этих соображений не ожидайте немедленной обратной связи о возможных ошибках сериализации ваших объектов при добавлении их в словари состояний, вам нужно будет перейти со страницы или переключиться на другое приложение, чтобы получать уведомления об ошибках.

Местом для обработки ошибок такого рода является событие UnhandledException класса приложения, поскольку этап сериализации находится вне вашего прямого контроля (если только вы не используете настраиваемый механизм сериализации перед добавлением объектов в словари состояний). Но если это произойдет, ваше приложение наверняка будет в нестабильном состоянии.

Чтобы избежать этих ошибок, если вы используете словари состояний для сохранения сложных структур данных (как, например, некоторые ViewModels), я бы предложил создать очень простой вспомогательный класс DataContractSerializer (например, тот, который вы можете найти в тестовом проекте) и используйте его для настройки проекта, чтобы проверить, может ли ваши объекты быть правильно сериализованы платформой.

Пример проекта: