c# - Deserialización XML de propiedad de colección con valores predeterminados
serialization xmlserializer (1)
Para la configuración de la aplicación, con frecuencia crearé una clase de configuración con valores de configuración para la aplicación que luego deserializaré en un objeto para utilizar. El objeto de configuración generalmente está enlazado a un control de interfaz de usuario para que el usuario pueda cambiar y mantener la configuración. La clase de configuración generalmente tiene valores predeterminados asignados a las propiedades para que siempre haya una configuración predeterminada. Esto ha funcionado bien. Recientemente tuve una situación en la que tenía una lista de cadenas que proporcionaban información de ruta predeterminada. Y lo que vi me hizo darme cuenta de que no sabía completamente cómo se pueblan las propiedades del objeto durante la deserialización de XML a un objeto.
Entonces creé un ejemplo simple para mostrar el comportamiento. La siguiente es una clase simple que tiene un par de propiedades que tienen algunos valores predeterminados de código.
[Serializable]
public class TestConfiguration
{
public String Name
{
get
{
return mName;
}
set
{
mName = value;
}
}private String mName = "Pete Sebeck";
public List<String> Associates
{
get
{
return mAssociates;
}
set
{
mAssociates = value;
}
} private List<String> mAssociates = new List<string>() { "Jon", "Natalie" };
public override String ToString()
{
StringBuilder buffer = new StringBuilder();
buffer.AppendLine(String.Format("Name: {0}", Name));
buffer.AppendLine("Associates:");
foreach(String associate in mAssociates)
{
buffer.AppendLine(String.Format("/t{0}", associate));
}
return buffer.ToString();
}
}
Y aquí hay un archivo principal que crea nuevos objetos, imprime el estado del objeto en la consola, lo serializa (xml) en un archivo, reconstituye un objeto de ese archivo e imprime nuevamente el estado del objeto en la consola. Lo que esperaba era un objeto que coincidiera con lo que se serializó. Lo que obtuve fue el objeto predeterminado con el contenido de la lista serializada agregada al predeterminado.
static void Main(string[] args)
{
// Create a default object
TestConfiguration configuration = new TestConfiguration();
Console.WriteLine(configuration.ToString());
// Serialize the object
XmlSerializer writer = new XmlSerializer(typeof(TestConfiguration));
StreamWriter filewriter = new StreamWriter("TestConfiguration.xml");
writer.Serialize(filewriter, configuration);
filewriter.Close();
// Now deserialize the xml into another object
XmlSerializer reader = new XmlSerializer(typeof(TestConfiguration));
StreamReader filereader = new StreamReader("TestConfiguration.xml");
TestConfiguration deserializedconfiguration = (TestConfiguration)reader.Deserialize(filereader);
filereader.Close();
Console.WriteLine(deserializedconfiguration.ToString());
Console.ReadLine();
}
Resultados:
Name: Pete Sebeck
Associates:
Jon
Natalie
Name: Pete Sebeck
Associates:
Jon
Natalie
Jon
Natalie
Supongo que siempre pensé que la propiedad List se establecería en lugar de agregarse a ella. ¿Alguien tiene un puntero al proceso de deserialización de colecciones? Al parecer, ahora sé los términos de búsqueda correctos ya que mis intentos están vacíos. Veo otras publicaciones que describen lo que estoy viendo y su enfoque para implementar la serialización. Estoy más buscando un puntero que describa lo que sucede cuando se deserializa una colección para poder explicarme lo que estoy viendo.
Tiene razón en que muchos serializadores (aunque no todos) funcionan de esta manera.
Json.NET sí, su método
JsonConverter.ReadJson
realidad tiene un valor
Object existingValue
para exactamente esta situación.
No conozco ningún documento donde se detallen este tipo de detalles de implementación.
La forma más fácil de determinar si un serializador usa colecciones preasignadas cuando está presente en lugar de asignarlo incondicionalmente y luego configurar uno mismo es probarlo realmente utilizando
ObservableCollection<T>
y adjuntando escuchas de depuración cuando se cambia:
[Serializable]
[DataContract]
public class TestConfiguration
{
[DataMember]
public String Name { get { return mName; } set { mName = value; } }
private String mName = "Pete Sebeck";
[DataMember]
public ObservableCollection<String> Associates
{
get
{
Debug.WriteLine(mAssociates == null ? "Associates gotten, null value" : "Associates gotten, count = " + mAssociates.Count.ToString());
return mAssociates;
}
set
{
Debug.WriteLine(value == null ? "Associates set to a null value" : "Associates set, count = " + value.Count.ToString());
RemoveListeners(mAssociates);
mAssociates = AddListeners(value);
}
}
private ObservableCollection<String> mAssociates = AddListeners(new ObservableCollection<string>() { "Jon", "Natalie" });
public override String ToString()
{
StringBuilder buffer = new StringBuilder();
buffer.AppendLine(String.Format("Name: {0}", Name));
buffer.AppendLine("Associates:");
foreach (String associate in mAssociates)
{
buffer.AppendLine(String.Format("/t{0}", associate));
}
return buffer.ToString();
}
static ObservableCollection<String> AddListeners(ObservableCollection<String> list)
{
if (list != null)
{
list.CollectionChanged -= list_CollectionChanged; // In case it was already there.
list.CollectionChanged += list_CollectionChanged;
}
return list;
}
static ObservableCollection<String> RemoveListeners(ObservableCollection<String> list)
{
if (list != null)
{
list.CollectionChanged -= list_CollectionChanged; // In case it was already there.
}
return list;
}
public static ValueWrapper<bool> ShowDebugInformation = new ValueWrapper<bool>(false);
static void list_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!ShowDebugInformation)
return;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Debug.WriteLine(string.Format("Added {0} items", e.NewItems.Count));
break;
case NotifyCollectionChangedAction.Move:
Debug.WriteLine("Moved items");
break;
case NotifyCollectionChangedAction.Remove:
Debug.WriteLine(string.Format("Removed {0} items", e.OldItems.Count));
break;
case NotifyCollectionChangedAction.Replace:
Debug.WriteLine("Replaced items");
break;
case NotifyCollectionChangedAction.Reset:
Debug.WriteLine("Reset collection");
break;
}
}
}
public static class TestTestConfiguration
{
public static void Test()
{
var test = new TestConfiguration();
Debug.WriteLine("/nTesting Xmlserializer...");
var xml = XmlSerializationHelper.GetXml(test);
using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
{
var testFromXml = XmlSerializationHelper.LoadFromXML<TestConfiguration>(xml);
Debug.WriteLine("XmlSerializer result: " + testFromXml.ToString());
}
Debug.WriteLine("/nTesting Json.NET...");
var json = JsonConvert.SerializeObject(test, Formatting.Indented);
using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
{
var testFromJson = JsonConvert.DeserializeObject<TestConfiguration>(json);
Debug.WriteLine("Json.NET result: " + testFromJson.ToString());
}
Debug.WriteLine("/nTesting DataContractSerializer...");
var contractXml = DataContractSerializerHelper.GetXml(test);
using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
{
var testFromContractXml = DataContractSerializerHelper.LoadFromXML<TestConfiguration>(contractXml);
Debug.WriteLine("DataContractSerializer result: " + testFromContractXml.ToString());
}
Debug.WriteLine("/nTesting BinaryFormatter...");
var binary = BinaryFormatterHelper.ToBase64String(test);
using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
{
var testFromBinary = BinaryFormatterHelper.FromBase64String<TestConfiguration>(binary);
Debug.WriteLine("BinaryFormatter result: " + testFromBinary.ToString());
}
Debug.WriteLine("/nTesting JavaScriptSerializer...");
var javaScript = new JavaScriptSerializer().Serialize(test);
using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
{
var testFromJavaScript = new JavaScriptSerializer().Deserialize<TestConfiguration>(javaScript);
Debug.WriteLine("JavaScriptSerializer result: " + testFromJavaScript.ToString());
}
}
}
Ejecuté la prueba anterior y encontré:
-
XmlSerializer
y Json.NET usan la colección preexistente si está presente. (En Json.NET esto se puede controlar configurandoJsonSerializerSettings.ObjectCreationHandling
paraReplace
) -
JavaScriptSerializer
,BinaryFormatter
yDataContractSerializer
no lo hacen, y siempre asignan la colección ellos mismos. Para los dos últimos esto no es sorprendente ya que ambos no llaman a los constructores predeterminados y simplemente asignan memoria vacía directamente.
No sé por qué los serializadores en el caso 1 se comportan de esta manera. ¿Quizás a sus autores les preocupaba que la clase que los contenía pudiera querer usar internamente una subclase de la colección que se deserializa, o adjuntar observadores a las colecciones observables como lo he hecho, y decidieron honrar ese diseño?
Una nota: para todos los serializadores (excepto, tal vez,
BinaryFormatter
, sobre los cuales no estoy seguro), si una propiedad de colección
se declara específicamente como una matriz,
entonces el serializador asignará la matriz en sí y establecerá la matriz después de que esté completamente poblada.
Esto significa que las
matrices siempre se pueden usar como colecciones proxy durante la serialización
.
Al usar una matriz de proxy, puede garantizar que su colección se sobrescriba durante la deserialización:
[IgnoreDataMember]
[XmlIgnore]
[ScriptIgnore]
public ObservableCollection<String> { get; set; } // Or List<string> or etc.
[XmlArray("Associates")]
[DataMember(Name="Associates")]
public string[] AssociateArray
{
get
{
return (Associates == null ? null : Associates.ToArray());
}
set
{
if (Associates == null)
Associates = new ObservableCollection<string>();
Associates.Clear();
if (value != null)
foreach (var item in value)
Associates.Add(item);
}
}
Ahora la colección vuelve con solo los miembros previamente serializados con los 5 serializadores.