Poniższy artykuł pochodzi z serii Przygotowań do egzaminu 70-536.
Niestandardowa serializacja pozwala na zwiększenie kontroli nad tym procesem. Dzięki niej możemy także osiągnąć kompatybilność między różnymi wersjami przekształcanych wcześniej obiektów. Mamy dwie metody przeprowadzenia takiej operacji: implementacja interfejsu ISerializable, oraz odpowiadanie na występujące zdarzenia.
Implementacja ISerializable
Wykorzystując interfejs oraz dodając atrybut Serializable możemy nadpisać domyślny mechanizm frameworka. Wymusza to na nas jednak kilka czynności:
- Implementacja metody GetObjectData, która jest wywoływana podczas serializacji. W przekazanym podczas wywołania obiekcie SerializationInfo ustawiamy wartości, które chcemy przekształcić.
- Wykorzystanie specjalnego konstruktora, który będzie wywołany przy deserializacji. Konstruktor taki musi akceptować dwa obiekty o typach SerializationInfo i StreamingContext.
Zakładamy, że mamy poniższą klasę:
1: class Person
2: {
3: private string _name;
4: private DateTime _dateOfBirth;
5:
6: public Person(string name, DateTime dateOfBirth)
7: {
8: _name = name;
9: _dateOfBirth = dateOfBirth;
10: }
11:
12: public string GetName() { return _name; }
13: public DateTime GetDateOfBirth() { return _dateOfBirth; }
14: }
Chcemy dodać do niej niestandardową serializację:
1: [Serializable]
2: class Person : ISerializable
3: {
4: private string _name;
5: private DateTime _dateOfBirth;
6:
7: public Person(string name, DateTime dateOfBirth)
8: {
9: _name = name;
10: _dateOfBirth = dateOfBirth;
11: }
12:
13: public string GetName() { return _name; }
14: public DateTime GetDateOfBirth() { return _dateOfBirth; }
15:
16: #region ISerializable Members
17:
18: public void GetObjectData(SerializationInfo info, StreamingContext context)
19: {
20: info.AddValue("Name", _name);
21: info.AddValue("DOB", _dateOfBirth);
22: }
23:
24: #endregion
25:
26: public Person(SerializationInfo info, StreamingContext context)
27: {
28: _name = info.GetString("Name");
29: _dateOfBirth = info.GetDateTime("DOB");
30: }
31: }
Serializacja i deserializacja:
1: Person p = new Person("Jan", new DateTime(2003, 3, 13));
2:
3: // Serializacje
4: Stream stream = File.Open("test.dat", FileMode.Create);
5: BinaryFormatter bFormatter = new BinaryFormatter();
6: bFormatter.Serialize(stream, p);
7: stream.Close();
8:
9: // Deserializacja
10: Person o;
11: Stream stream2 = File.Open("test.dat", FileMode.Open);
12: BinaryFormatter bFormatter2 = new BinaryFormatter();
13: o = (Person)bFormatter2.Deserialize(stream2);
14: stream2.Close();
15:
16: Console.Write("{0}, ur. {1}",
17: o.GetName(),
18: o.GetDateOfBirth().ToShortDateString()
19: ); // Jan, ur. 2003-03-13
Odpowiadanie na zdarzenia
Jeśli korzystamy z klasy BinaryFormatter mamy możliwość korzystania ze zdarzeń, które wspierane są przez .NET Framework. Zdarzenia wywołują metody w naszej klasie, które oznaczone są specjalnymi atrybutami. Mamy cztery typu zdarzeń:
- Serializing – występuje przed serializacją, metoda oznaczana atrybutem OnSerialazing,
- Serialized – występuje po serialiacji, metoda oznaczana atrybutem OnSerialized,
- Deserializing – występuje przed deserializacją, metoda oznaczana atrybutem OnDeserializing,
- Deserialized – występuje po deserializacji i po wywołaniu IDeserializationCallback.OnDeserialization, metoda klasy zostanie wywołana, jeśli zostanie oznaczona atrybutem OnDeserialized.
Metoda, która ma być wywołana po wystąpieniu zdarzenia musi spełniać pewne wymagania:
- Przyjmuje obiekt StreamingContext jako parametr,
- Zwraca void,
- Poprzedzona jest atrybutem łączącym ją ze zdarzeniem.
Poniższy przykład pokazuje jak utworzyć obiekt, który odpowiada na zdarzenia serializacji:
1: [Serializable]
2: class ShoppingCartItem
3: {
4: public Int32 productId;
5: public decimal price;
6: public Int32 quantity;
7: public decimal total;
8:
9: [OnSerializing]
10: void CalculateTotal(StreamingContext sc)
11: {
12: total = price * quantity;
13: }
14:
15: [OnDeserialized]
16: void CheckTotal(StreamingContext sc)
17: {
18: if (total == 0) { CalculateTotal(sc); }
19: }
20: }
Podsumowanie
W większości wypadków standardowe metody serializacji powinny być wystarczające. Jeżeli chcemy mieć większą władzę możemy wykorzystać bardziej zaawansowane mechanizmy. Implementując interfejs IFormatter lub IGenericFormatter możemy osiągnąć “całkowitą” kontrolę.
Artykuł ten jest ostatnim na temat serializowania obiektów. Następny wpis będzie dotyczył grafiki.
Kolejny artykuł z serii 70-536: Drawing graphics