System.Runtime.Serialization broken with SoapFormatter
I'm working on a Windows Forms Application targeting .NET Framework 4.8. There seems to be a breakdown in System.Runtime.Serialization using Formatters.Soap, and that breakdown appears to be somewhere in the underlying Framework. I can't figure it out on my own.
The problem is that the CONTAINER object is fully deserialized, and its ISerializable constructor has completely exited, before the Framework decides to call the ISerializable constructor on the object(s) contained within the container.
The idea here is to present a persistent-options-storage facility that doesn't bury its files under %AppData% but saves them into My Documents, where new and different options sets can be spun up just by instantiating a new object and assigning values, to avoid the tedious work of creating a brand new Serializable object for each and every new options group that comes up during programming.
You should be able to run these code files with only the default imports given by VS2022. You will need to add a Reference to System.Runtime.Serialization.Formatters.Soap.
I really hope I just missed a step!
/// <summary>
/// <para>
/// The class which provides a value-type-agnostic Serializable wrapper for individual options.
/// </para>
/// </summary>
[Serializable]
public class Class_Settings_Item : System.Runtime.Serialization.ISerializable
{
public String name = null;
/// <summary>
/// <para>
/// Item values which are not serializable must be cast to a type which ARE serializable.
/// This includes items whose Type.IsSerializable field is TRUE -- this field is
/// just a lie, for example with List<T>
/// </para>
/// </summary>
public Object value = null;
public Type valueType = null;
public Class_Settings_Item( String item_name, Object item_value )
{
this.name = item_name;
this.valueType = item_value.GetType();
this.value = item_value;
}
public Class_Settings_Item( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context )
{
this.name = ( String ) info.GetValue( @"name", typeof( String ) );
this.valueType = ( Type ) info.GetValue( @"valueType", typeof( Type ) );
this.value = info.GetValue( @"value", this.valueType );
// This constructor is never called until after the container's ISerializable constructor
// has completely exited. This constructor does in fact work just fine, it just never
// gets called until the higher-level container's constructor has already exited.
// Set a breakpoint below to demonstrate.
}
public void GetObjectData( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context )
{
info.AddValue( @"name", this.name );
info.AddValue( @"valueType", this.valueType );
info.AddValue( @"value", this.value );
}
}
/// <summary>
/// <para>
/// The class which provides a basic container for on-the-fly persistence generation.
/// </para>
/// </summary>
[Serializable]
public partial class Class_SettingsBase : System.Runtime.Serialization.ISerializable
{
/// <summary>
/// <para>
/// Indicates whether the file has been saved since the last change was made to the object in memory.
/// </para>
/// </summary>
[ NonSerialized]
protected Boolean isSaved = true;
/// <summary>
/// <para>
/// Gets a value which indicates whether the file has been saved since the last change was made to the object in memory.
/// </para>
/// </summary>
public Boolean IsSaved
{
get
{
return this.isSaved;
}
}
/// <summary>
/// <para>
/// Contains the filesystem path to the file this was last loaded from or saved to.
/// </para>
/// <remarks>
/// This is included in PROJECT settings, rather than Settings or ProgramSettings, because
/// while there is only ever going to be one ProgramSettings instance, the Program may then
/// have multiple Projects open at one time.
/// </remarks>
/// </summary>
[NonSerialized]
protected string lastSavedFilePath = null;
/// <summary>
/// <para>
/// The underlying collection of objects which represent saved settings data.
/// </para>
/// <para>
/// Item values which are not serializable must be cast to a type which ARE serializable.
/// This includes items whose Type.IsSerializable field is TRUE -- this field is
/// just a lie, for example with List<T>
/// </para>
/// </summary>
protected List< Class_Settings_Item > settings = new List< Class_Settings_Item >();
/// <summary>
/// <para>
/// Gets or sets the underlying collection of objects which represent saved settings data.
/// </para>
/// <para>
/// Item values which are not serializable must be cast to a type which ARE serializable.
/// This includes items whose Type.IsSerializable field is TRUE -- this field is
/// just a lie, for example with List<T>
/// </para>
/// </summary>
public List<Class_Settings_Item> Settings
{
get
{
return this.settings;
}
set
{
this.settings = value;
this.isSaved = false;
}
}
public Class_Settings_Item Get( String name )
{
Class_Settings_Item r = null;
for (Int32 i = 0; i < this.settings.Count; i++ )
{
if ( this.settings[ i ].name == name )
{
r = this.settings[ i ];
break;
}
}
return r;
}
public void Add( Class_Settings_Item item )
{
Class_Settings_Item existing = this.Get( item.name );
if ( existing != null )
{
existing = item;
}
else
{
this.settings.Add( item );
}
}
public void Insert( Int32 index, Class_Settings_Item item )
{
Class_Settings_Item existing = this.Get( item.name );
if ( existing != null )
{
this.settings.Remove( existing );
}
this.settings.Insert( index, item );
}
public void Remove( String name )
{
Class_Settings_Item existing = this.Get( name );
if ( existing != null )
{
this.settings.Remove( existing );
}
}
public void Remove( Class_Settings_Item item )
{
this.settings.Remove( item );
}
public void RemoveAt( Int32 index )
{
this.settings.RemoveAt( index );
}
public void Clear()
{
this.settings.Clear();
}
public Class_SettingsBase()
{
}
public Class_SettingsBase( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context )
{
Class_Settings_Item[] arr = ( Class_Settings_Item[] )info.GetValue( @"settings", typeof( Class_Settings_Item[] ) );
// Right now, arr does contain one item, but that item is NULL
// You can set a breakpoint below to demonstrate
this.settings = new List<Class_Settings_Item>( arr );
// The constructor on the underlying Class_SettingsItem object (contained within the deserialized array)
// won't be called until after this function exits.
// Set a breakpoint below to demonstrate.
}
public void GetObjectData( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context )
{
info.AddValue( @"settings", this.settings.ToArray() );
}
public void Reload()
{
Class_SettingsBase csb = Class_SettingsBase.LoadFrom( this.lastSavedFilePath );
this.settings = csb.settings;
}
public void SaveAs( String filename )
{
using ( System.IO.FileStream fs = new System.IO.FileStream( filename, System.IO.FileMode.OpenOrCreate ) )
{
System.Runtime.Serialization.Formatters.Soap.SoapFormatter formatter = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
formatter.Serialize( fs, this );
}
this.lastSavedFilePath = filename;
this.isSaved = true;
}
public void Save()
{
this.SaveAs( this.lastSavedFilePath );
}
public static Class_SettingsBase LoadFrom( String filename )
{
Class_SettingsBase r = null;
using ( System.IO.FileStream fs = new System.IO.FileStream( filename, System.IO.FileMode.Open ) )
{
System.Runtime.Serialization.Formatters.Soap.SoapFormatter formatter = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
r = ( Class_SettingsBase ) formatter.Deserialize( fs );
}
r.lastSavedFilePath = filename;
return r;
}
}
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main( )
{
// Make sure you edit these paths to some place you can read/write files
String
testXML0 = @"\path\to\test0.xml",
testXML1 = @"\path\to\test1.xml"
;
// This works just fine.
Class_SettingsBase testing = new Class_SettingsBase();
testing.Add( new Class_Settings_Item( @"stringArr", new String[] { "wakkawakka", "tikki", "takka" } ) );
testing.SaveAs( testXML0 );
// At this point, you can examine the first output XML file to verify that everything works.
// Set a breakpoint below to demonstrate the problem. When this call is made, it begins to
// deserialize the Class_SettingsBase instance stored in the xml file, but the deserialization
// process does not include deserializing the values stored in the array.
Class_SettingsBase testing2 = Class_SettingsBase.LoadFrom( testXML0 ) );
// Set a breakpoint below to examine the testing2 instance, and verify that its list container has
// one element which is NULL.
testing2.SaveAs( testXML1 );
// At this point, you can examine the second output XML file to verify that things did not work properly.
// The next 3 lines are specific to the Windows Forms Application.
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault( false );
Application.Run( new Form_MainGUI() );
}
}