Blogginlägg

Problem med att serialisera properties

Av Cecilia Wirén | Blogg | 3 april 2013

Om du har serialiserat en klass med hjälp av en IFormatter så kommer du få problem med att göra om automatic properties till vanliga properties. Dessa kommer inte att serialiseras tillbaka automatiskt.
Det finns flera sätt att serialisera och det kanske kan vara svårt att veta att det är just en IFormatter som har används men en ledtråd är att du var tvungen att sätta attributet Serializable på klassen för att få det att fungera. Eller om du själv styrt över koden som serialiserat så har du använt en BinaryFormatter eller någon annan klass som implementerar IFormatter.

Själva problemet är alltså att ha en klass liknande:

[Serializable]
public class Person
{
    public string Name { get; set; }
}

Som du sedan försöker skriva för att kunna lägga in mer logik, tex så som validering eller PropertyChange event:
[Serializable]
public class Person

{
    private string _name;

    public string Name

    {
        get { return _name; }
        set { _name = value; }
    }
}

Vid deserialisering av data som serialiserats från den första klassen och sedan skall deserialiseras tillbaka med den uppdaterade klassen, kommer inte att lyckas fylla Name propertien. Varför?
Jo, En IFormatter serialiserar ALLA fält i en klass dvs. alla klassvariabler. I den första klassen har du ju egentligen inga sådana, bara properties. Så kallade automatic properties. Finessen med automatic propeties är att slippa skriva den enkla koden med ett fält för att sedan bara mappa ut den via en property. .Net behöver dock fortfarande en variabel att lagra värdet i. Så kompilatorn kommer generera fram fälten. Eftersom sedan IFormatter:n går fält för fält och matchar namn så kommer din omgjorda property inte att fungerar trots samma namn eftersom du själv måste tillverka ett fält för förvaringen av värdet.
Den lätta lösningen på detta skulle då kunna vara att använda samma namn som .NETs automatiska framgenererade namn. Det namnet är "<Name>k__BackingField" Vilket tyvärr innehåller för oss otillåtna tecken för variabelnamn. Det du får göra för att få det att fungera är att anpassa serialiseringen, dvs du skall bestämma vad som skall spara undan och vad som läses upp samt i vilka fält. För att göra detta skall du lägga på interfacet ISerializing på klassen och implementera funktionen, GetObject, samt skriva en konstruktor med samma inparametrar. Det du inte får glömma bort är att om du tidigare inte hade någon konstruktor kommer du "förstöra" den automatgenerade default konstruktorn och får nu skriva den själv.

using System.Runtime.Serialization;

    [Serializable]
    public class Person: ISerializable
    {
        private string _name;

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        public Person() { }

        public Person(SerializationInfo info, StreamingContext context)
        {
            //deserialization code here
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            //serialization code here
        }
    }

IFormatter:n kommer nu inte att samla in data från alla fält utan kommer istället att anropa funktionen GetObject. Vid desarialisering kommer den nya konstruktorn att användas. I SerializationInfo objektet tar ut vad som sparats från en lista där varje värde har en så kallad nyckel. Vi behöver också ta hänsyn till om det sparade data kommer från den första versionen av klassen eller den andra. Finns inte värdet vi vill ha från SerilaiszationInfo objektet kastas ett exception så för att hantera båda versionerna av klassen så får vi göra “try-try again”. För att optimera lite så skall du se till att lägga uppslaget av den nyckeln som är mest troligt att finnas först eftersom ett exception alltid slöar ner koden. Så för vår nya klass skulle det se ut som följer:
 
public Person(SerializationInfo info, StreamingContext context)
{
    try
    {
        _name = info.GetString("name");
    }
    catch (SerializationException)
    {
        _name = info.GetString("<Name>k__BackingField");
    }
}

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
    info.AddValue("name", _name);
}

Glöm inte heller att ta med eventuellt andra fält som du vill ha serialiserade. Är det basklasser inblandade måste dessa fält också skrivas med, de hämtas inte automatiskt. Om det var en automatic property i en basklass som blev omskriven så har det genererade fältet ett lite annorlunda namn i formen av “BasklassNamn+<PropertyNamn>k_BakingField”. Dvs om vi hade haft en automatic property med namnet Id i basklassen PersonBase hade den hettat “PersonBase+<Id>k_BakingField”.
Skulle det vara några problem med att få till fältnamnen rätt eller att du bara funderar på vad som egentligen finns i SerializationInfo objektet så kan du alltid debugga. Det som är intressant hittar du under ‘Non-Public members’ delen och MemberNames och MemberValues.

serializationProperties


Till inlägget