commit ce8df64be5b31d7859e9ab963050308b6aeb77a2 Author: Markus Ewald Date: Wed Feb 28 20:20:50 2007 +0000 Imported newly created Nuclex.Support library which contains various supporting classes not related to a specific topic git-svn-id: file:///srv/devel/repo-conversion/nusu@1 d2e56fa2-650e-0410-a79f-9358c0239efd diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj new file mode 100644 index 0000000..3949a0d --- /dev/null +++ b/Nuclex.Support.csproj @@ -0,0 +1,114 @@ + + + {A696702E-AFAD-45E7-88FA-1E2520E5E746} + {9F340DF3-2AED-4330-AC16-78AC2D9B4738};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + Library + Properties + Nuclex.Support + Nuclex.Support + v1.0 + Windows + Microsoft.Xna.Framework.Content.Pipeline.EffectImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.FBXImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.TextureImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.XImporter.dll + + + + + true + full + false + bin\x86\Debug + TRACE;DEBUG;UNITTEST + prompt + 4 + true + false + x86 + Documentation\Nuclex.Support.xml + + + pdbonly + true + bin\x86\Release + TRACE + prompt + 4 + true + false + x86 + Documentation\Nuclex.Support.xml + + + + False + + + False + + + False + + + False + References\nmock\net-2.0\nmock.dll + + + False + References\nunit\net-2.0\nunit.framework.dll + True + + + False + + + + + + + false + Parentable + + + false + ParentingCollection + + + false + PriorityQueue + + + false + RingMemoryStream + + + + false + RingMemoryStream.Test + RingMemoryStream.cs + + + false + BinarySerializer.Test + BinarySerializer.cs + + + false + IBinarySerializable + + + false + BinarySerializer + + + + + + + \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b6e4be6 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Nuclex.Support")] +[assembly: AssemblyProduct("Nuclex.Support")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("Nuclex Development Labs")] +[assembly: AssemblyCopyright("Copyright © Nuclex Development Labs 2007")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1308e4c3-a0c1-423a-aaae-61c7314777e0")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/Source/Collections/Parentable.cs b/Source/Collections/Parentable.cs new file mode 100644 index 0000000..73cdb4e --- /dev/null +++ b/Source/Collections/Parentable.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Base class for objects that can be parented to an owner + /// Type of the parent object + public class Parentable + where ParentType : class { + + /// Assigns a new parent to this instance + internal void SetParent(ParentType parent) { + ParentType oldParent = this.parent; + this.parent = parent; + + OnParentChanged(oldParent); + } + + /// The parent object that owns this instance + protected ParentType Parent { + get { return this.parent; } + } + + /// Invoked whenever the instance's owner changes + /// + /// When items are parented for the first time, the oldParent argument will + /// be null. Also, if the element is removed from the collection, the + /// current parent will be null. + /// + /// Previous owner of the instance + protected virtual void OnParentChanged(ParentType oldParent) { } + + /// Current parent of this object + private ParentType parent; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ParentingCollection.cs b/Source/Collections/ParentingCollection.cs new file mode 100644 index 0000000..e546539 --- /dev/null +++ b/Source/Collections/ParentingCollection.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Reflection; + +namespace Nuclex.Support.Collections { + + /// Collection that automatically assigns an owner to all its elements + /// + /// This collection automatically assigns a parent object to elements that + /// are managed in it. The elements have to derive from the Parentable<> + /// base class. + /// + /// Type of the parent object to assign to items + /// Type of the items being managed in the collection + public class ParentingCollection : Collection, IDisposable + where ItemType : Parentable + where ParentType : class { + + /// Called when the object is garbage-collected + ~ParentingCollection() { + Dispose(false); // called from GC + } + + /// Reparents all elements in the collection + /// New parent to take ownership of the items + internal void Reparent(ParentType parent) { + this.parent = parent; + + for(int index = 0; index < Count; ++index) + base[index].SetParent(parent); + } + + /// Called when the asset needs to release its resources + /// + /// Whether the mehod has been called from user code. If this argument + /// is false, the object is being disposed by the garbage collection and + /// it mustn't access other objects (including the attempt to Dispose() them) + /// as these might have already been destroyed by the GC. + /// + protected virtual void Dispose(bool calledByUser) { + + // Only destroy the other resources when we're not being called from + // the garbage collector, otherwise we'd risk accessing objects that + // have already been disposed + if(calledByUser) { + + // Have the items do their cleanup work + Reparent(null); + + // Dispose of all the items in the collection + foreach(ItemType item in this) { + IDisposable disposable = item as IDisposable; + if(disposable != null) + disposable.Dispose(); + } + + // Remove all items from the collection + base.ClearItems(); + + } + + } + + /// Clears all elements from the collection + protected override void ClearItems() { + for(int index = 0; index < Count; ++index) + base[index].SetParent(null); + + base.ClearItems(); + } + + /// Inserts a new element into the collection + /// Index at which to insert the element + /// Item to be inserted + protected override void InsertItem(int index, ItemType item) { + base.InsertItem(index, item); + item.SetParent(this.parent); + } + + /// Removes an element from the collection + /// Index of the element to remove + protected override void RemoveItem(int index) { + base[index].SetParent(null); + base.RemoveItem(index); + } + + /// Takes over a new element that is directly assigned + /// Index of the element that was assigned + /// New item + protected override void SetItem(int index, ItemType item) { + base.SetItem(index, item); + item.SetParent(this.parent); + } + + /// Release all resources owned by the instance explicitely + void IDisposable.Dispose() { + Dispose(true); // Called by user + } + + /// Parent this collection currently belongs to + private ParentType parent; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/PriorityQueue.cs b/Source/Collections/PriorityQueue.cs new file mode 100644 index 0000000..eba6207 --- /dev/null +++ b/Source/Collections/PriorityQueue.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { +/* + /// Queue that dequeues items in order of their priority + public class PriorityQueue : ICollection, IEnumerable { + + public static PriorityQueue Syncronized(PriorityQueue P) { + return new PriorityQueue(ArrayList.Synchronized(P.InnerList), P.Comparer, false); + } + public static PriorityQueue ReadOnly(PriorityQueue P) { + return new PriorityQueue(ArrayList.ReadOnly(P.InnerList), P.Comparer, false); + } + + public PriorityQueue() + : this(Comparer.Default) { } + + public PriorityQueue(int C) + : this(Comparer.Default, C) { } + + public PriorityQueue(IComparer c) { + Comparer = c; + } + + public PriorityQueue(IComparer c, int Capacity) { + Comparer = c; + InnerList.Capacity = Capacity; + } + + protected PriorityQueue(ArrayList Core, IComparer Comp, bool Copy) { + if(Copy) + InnerList = Core.Clone() as ArrayList; + else + InnerList = Core; + Comparer = Comp; + } + + protected void SwitchElements(int i, int j) { + object h = InnerList[i]; + InnerList[i] = InnerList[j]; + InnerList[j] = h; + } + + protected virtual int OnCompare(int i, int j) { + return Comparer.Compare(InnerList[i], InnerList[j]); + } + + #region public methods + /// + /// Push an object onto the PQ + /// + /// The new object + /// + /// The index in the list where the object is _now_. This will change when + /// objects are taken from or put onto the PQ. + /// + public int Queue(object O) { + int p = InnerList.Count, p2; + InnerList.Add(O); // E[p] = O + do { + if(p == 0) + break; + p2 = (p - 1) / 2; + if(OnCompare(p, p2) < 0) { + SwitchElements(p, p2); + p = p2; + } else + break; + } while(true); + return p; + } + + /// + /// Get the smallest object and remove it. + /// + /// The smallest object + public object Dequeue() { + object result = InnerList[0]; + int p = 0, p1, p2, pn; + InnerList[0] = InnerList[InnerList.Count - 1]; + InnerList.RemoveAt(InnerList.Count - 1); + do { + pn = p; + p1 = 2 * p + 1; + p2 = 2 * p + 2; + if(InnerList.Count > p1 && OnCompare(p, p1) > 0) // links kleiner + p = p1; + if(InnerList.Count > p2 && OnCompare(p, p2) > 0) // rechts noch kleiner + p = p2; + + if(p == pn) + break; + SwitchElements(p, pn); + } while(true); + return result; + } + + /// + /// Notify the PQ that the object at position i has changed + /// and the PQ needs to restore order. + /// Since you dont have access to any indexes (except by using the + /// explicit IList.this) you should not call this function without knowing exactly + /// what you do. + /// + /// The index of the changed object. + public void Update(int i) { + int p = i, pn; + int p1, p2; + do // aufsteigen + { + if(p == 0) + break; + p2 = (p - 1) / 2; + if(OnCompare(p, p2) < 0) { + SwitchElements(p, p2); + p = p2; + } else + break; + } while(true); + if(p < i) + return; + do // absteigen + { + pn = p; + p1 = 2 * p + 1; + p2 = 2 * p + 2; + if(InnerList.Count > p1 && OnCompare(p, p1) > 0) // links kleiner + p = p1; + if(InnerList.Count > p2 && OnCompare(p, p2) > 0) // rechts noch kleiner + p = p2; + + if(p == pn) + break; + SwitchElements(p, pn); + } while(true); + } + + /// + /// Get the smallest object without removing it. + /// + /// The smallest object + public object Peek() { + if(InnerList.Count > 0) + return InnerList[0]; + return null; + } + + public bool Contains(object value) { + return InnerList.Contains(value); + } + + public void Clear() { + InnerList.Clear(); + } + + public int Count { + get { + return InnerList.Count; + } + } + IEnumerator IEnumerable.GetEnumerator() { + return InnerList.GetEnumerator(); + } + + public void CopyTo(Array array, int index) { + InnerList.CopyTo(array, index); + } + + public object Clone() { + return new PriorityQueue(InnerList, Comparer, true); + } + + public bool IsSynchronized { + get { + return InnerList.IsSynchronized; + } + } + + public object SyncRoot { + get { + return this; + } + } + #endregion + #region explicit implementation + bool IList.IsReadOnly { + get { + return false; + } + } + + object IList.this[int index] { + get { + return InnerList[index]; + } + set { + InnerList[index] = value; + Update(index); + } + } + + int IList.Add(object o) { + return Queue(o); + } + + void IList.RemoveAt(int index) { + throw new NotSupportedException(); + } + + void IList.Insert(int index, object value) { + throw new NotSupportedException(); + } + + void IList.Remove(object value) { + throw new NotSupportedException(); + } + + int IList.IndexOf(object value) { + throw new NotSupportedException(); + } + + bool IList.IsFixedSize { + get { + return false; + } + } + #endregion + + protected ArrayList InnerList = new ArrayList(); + protected IComparer Comparer; + } +*/ +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/RingMemoryStream.Test.cs b/Source/Collections/RingMemoryStream.Test.cs new file mode 100644 index 0000000..f73b207 --- /dev/null +++ b/Source/Collections/RingMemoryStream.Test.cs @@ -0,0 +1,139 @@ +using System; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the ring buffer class + [TestFixture] + public class RingMemoryStreamTest { + + /// Prepares some test data for the units test methods + [TestFixtureSetUp] + public void Setup() { + this.testBytes = new byte[20]; + for(int i = 0; i < 20; ++i) + this.testBytes[i] = (byte)i; + } + + /// + /// Ensures that the ring buffer blocks write attempts that would exceed its capacity + /// + [Test, ExpectedException(typeof(OverflowException))] + public void TestTooLargeChunk() { + new RingMemoryStream(10).Write(this.testBytes, 0, 11); + } + + /// + /// Ensures that the ring buffer still accepts write attempts what would fill the + /// entire buffer in one go. + /// + [Test] + public void TestBarelyFittingChunk() { + new RingMemoryStream(10).Write(this.testBytes, 0, 10); + } + + /// Tests whether the ring buffer correctly handles fragmentation + [Test] + public void TestSplitBlockRead() { + RingMemoryStream rms = new RingMemoryStream(10); + rms.Write(this.testBytes, 0, 10); + rms.Read(this.testBytes, 0, 5); + rms.Write(this.testBytes, 0, 5); + + byte[] actual = new byte[10]; + rms.Read(actual, 0, 10); + Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9, 0, 1, 2, 3, 4 }, actual); + } + + /// + /// Tests whether the ring buffer correctly returns partial data if more + /// data is requested than is contained in it. + /// + [Test] + public void TestEndOfStream() { + byte[] temp = new byte[10]; + + RingMemoryStream rms = new RingMemoryStream(10); + Assert.AreEqual(0, rms.Read(temp, 0, 5)); + + rms.Write(this.testBytes, 0, 5); + Assert.AreEqual(5, rms.Read(temp, 0, 10)); + + rms.Write(this.testBytes, 0, 6); + rms.Read(temp, 0, 5); + rms.Write(this.testBytes, 0, 9); + Assert.AreEqual(10, rms.Read(temp, 0, 20)); + } + + /// + /// Validates that the ring buffer can extend its capacity without loosing data + /// + [Test] + public void TestCapacityIncrease() { + RingMemoryStream rms = new RingMemoryStream(10); + rms.Write(this.testBytes, 0, 10); + + rms.Capacity = 20; + byte[] actual = new byte[10]; + rms.Read(actual, 0, 10); + + Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); + } + + /// + /// Validates that the ring buffer can reduce its capacity without loosing data + /// + [Test] + public void TestCapacityDecrease() { + RingMemoryStream rms = new RingMemoryStream(20); + rms.Write(this.testBytes, 0, 10); + + rms.Capacity = 10; + byte[] actual = new byte[10]; + rms.Read(actual, 0, 10); + + Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); + } + + /// + /// Checks that an exception is thrown when the ring buffer's capacity is + /// reduced so much it would have to give up some of its contained data + /// + [Test, ExpectedException(typeof(ArgumentOutOfRangeException))] + public void TestCapacityDecreaseException() { + RingMemoryStream rms = new RingMemoryStream(20); + rms.Write(this.testBytes, 0, 20); + + rms.Capacity = 10; + } + + /// + /// Tests whether the auto reset feature works (resets the buffer point to the + /// left end of the buffer when it gets empty; mainly a performance feature). + /// + [Test] + public void TestAutoReset() { + RingMemoryStream rms = new RingMemoryStream(10); + + byte[] temp = new byte[10]; + + rms.Write(this.testBytes, 0, 8); + rms.Read(temp, 0, 2); + rms.Read(temp, 0, 2); + rms.Read(temp, 0, 1); + rms.Read(temp, 0, 1); + + Assert.AreEqual(2, rms.Length); + } + + /// Test data for the ring buffer unit tests + private byte[] testBytes; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/RingMemoryStream.cs b/Source/Collections/RingMemoryStream.cs new file mode 100644 index 0000000..a15f244 --- /dev/null +++ b/Source/Collections/RingMemoryStream.cs @@ -0,0 +1,215 @@ +using System; +using System.IO; + +namespace Nuclex.Support.Collections { + + /// Specialized memory stream for ring buffers + public class RingMemoryStream : Stream { + + /// Initializes a new ring memory stream + /// Maximum capacity of the stream + public RingMemoryStream(int capacity) { + this.ringBuffer = new MemoryStream(capacity); + this.ringBuffer.SetLength(capacity); + this.empty = true; + } + + /// Maximum amount of data that will fit into the ring memory stream + public long Capacity { + get { return this.ringBuffer.Length; } + set { + int length = (int)Length; + if(value < length) + throw new ArgumentOutOfRangeException( + "New capacity is less than the stream's length" + ); + + // This could be done in a more efficient manner than just replacing + // the stream, but this operation will probably be called only once + // during the lifetime of the application -- if at all... + MemoryStream newBuffer = new MemoryStream((int)value); + + newBuffer.SetLength(value); + if(length > 0) + Read(newBuffer.GetBuffer(), 0, length); + + this.ringBuffer.Close(); + this.ringBuffer = newBuffer; + this.startIndex = 0; + this.endIndex = length; + } + + } + + /// Whether it's possible to read from this stream + public override bool CanRead { get { return true; } } + /// Whether this stream supports random access + public override bool CanSeek { get { return false; } } + /// Whether it's possible to write into this stream + public override bool CanWrite { get { return true; } } + /// Flushes the buffers and writes down unsaved data + public override void Flush() { } + + /// Current length of the stream + public override long Length { + get { + if((this.endIndex > this.startIndex) || this.empty) { + return this.endIndex - this.startIndex; + } else { + return this.ringBuffer.Length - this.startIndex + this.endIndex; + } + } + } + + /// Current cursor position within the stream + public override long Position { + get { throw new NotSupportedException("The ring buffer does not support seeking"); } + set { throw new NotSupportedException("The ring buffer does not support seeking"); } + } + + /// Reads data from the beginning of the stream + /// Buffer in which to store the data + /// Starting index at which to begin writing the buffer + /// Number of bytes to read from the stream + /// Die Number of bytes actually read + public override int Read(byte[] buffer, int offset, int count) { + + // The end index lies behind the start index (usual case), so the + // ring memory is not fragmented. Example: |-----<#######>-----| + if((this.startIndex < this.endIndex) || this.empty) { + + // The Stream interface requires us to return less than the requested + // number of bytes if we don't have enough data + count = Math.Min(count, this.endIndex - this.startIndex); + if(count > 0) { + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, count); + this.startIndex += count; + + if(this.startIndex == this.endIndex) + setEmpty(); + } + + // If the end index lies before the start index, the data in the + // ring memory stream is fragmented. Example: |#####>-------<#####| + } else { + int linearAvailable = (int)(this.ringBuffer.Length - this.startIndex); + + // Will this read process cross the end of the ring buffer, requiring us to + // read the data in 2 steps? + if(count > linearAvailable) { + + // The Stream interface requires us to return less than the requested + // number of bytes if we don't have enough data + count = Math.Min(count, linearAvailable + this.endIndex); + + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, linearAvailable); + this.ringBuffer.Position = 0; + this.startIndex = count - linearAvailable; + this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex); + + // Nope, the amount of requested data can be read in one piece without + // crossing the end of the ring buffer + } else { + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, count); + this.startIndex += count; + + } + + if(this.startIndex == this.endIndex) + setEmpty(); + } + + return count; + } + + /// Appends data to the end of the stream + /// Buffer containing the data to append + /// Starting index of the data in the buffer + /// Number of bytes to write to the stream + public override void Write(byte[] buffer, int offset, int count) { + + // The end index lies behind the start index (usual case), so the + // ring memory is not fragmented. Example: |-----<#######>-----| + if((this.startIndex < this.endIndex) || this.empty) { + int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex); + + // If the data to be written would cross the ring memory stream's end, + // we have to check that there's enough space at the beginning of the + // stream to contain the remainder of the data. + if(count > linearAvailable) { + if(count > (linearAvailable + this.startIndex)) + throw new OverflowException("Data does not fit in Ringbuffer"); + + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, linearAvailable); + this.ringBuffer.Position = 0; + this.endIndex = count - linearAvailable; + this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex); + + // All data can be appended at the current stream position without + // crossing the ring memory stream's end + } else { + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, count); + this.endIndex += count; + } + + this.empty = false; + + // If the end index lies before the start index, the ring memory stream + // has been fragmented. Hence, this means the gap into which we are about + // to write cannot be fragmented. Example: |#####>-------<#####| + } else { + if(count > (this.startIndex - this.endIndex)) + throw new OverflowException("Data does not fit in Ringbuffer"); + + // Because the gap isn't fragmented, we can be sure that a single + // write call will suffice. + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, count); + this.endIndex += count; + } + + } + + /// Jumps to the specified location within the stream + /// Position to jump to + /// Origin towards which to interpret the offset + /// The new offset within the stream + public override long Seek(long offset, SeekOrigin origin) { + throw new NotSupportedException("The ring buffer does not support seeking"); + } + + /// Changes the length of the stream + /// New length to resize the stream to + public override void SetLength(long value) { + throw new NotSupportedException("This operation is not supported"); + } + + /// Resets the stream to its empty state + private void setEmpty() { + this.empty = true; + this.startIndex = 0; + this.endIndex = 0; + } + + /// Internal stream containing the ring buffer data + private MemoryStream ringBuffer; + /// Start index of the data within the ring buffer + private int startIndex; + /// End index of the data within the ring buffer + private int endIndex; + /// Whether the ring buffer is empty + /// + /// This field is required to differentiate between the ring buffer being + /// filled to the limit and being totally empty in the case that + /// the start index and the end index are the same. + /// + bool empty; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Serialization/BinarySerializer.Test.cs b/Source/Serialization/BinarySerializer.Test.cs new file mode 100644 index 0000000..8581221 --- /dev/null +++ b/Source/Serialization/BinarySerializer.Test.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Serialization { + + /// Ensures that the binary serializer is working correctly + [TestFixture] + public class BinarySerializerTest { + + private class TestSerializable : IBinarySerializable { + public void Load(BinaryReader reader) { this.Dummy = reader.ReadInt32(); } + public void Save(BinaryWriter writer) { writer.Write(this.Dummy); } + public int Dummy; + } + + /// Prepares some test data for the units test methods + [TestFixtureSetUp] + public void Setup() { + } + + /// + /// Ensures that the ring buffer blocks write attempts that would exceed its capacity + /// + [Test] + public void TestTooLargeChunk() { + MemoryStream buffer = new MemoryStream(); + + // Fill and save + { + List serializables = new List(); + + serializables.Add(new TestSerializable()); + serializables.Add(new TestSerializable()); + serializables[0].Dummy = 123; + serializables[1].Dummy = 456; + + BinarySerializer.SaveCollection( + serializables, new BinaryWriter(buffer) + ); + buffer.Position = 0; + } + + // Load and validate + { + List serializables = new List(); + + BinarySerializer.LoadCollection( + serializables, new BinaryReader(buffer) + ); + + Assert.AreEqual(2, serializables.Count); + Assert.AreEqual(123, serializables[0].Dummy); + Assert.AreEqual(456, serializables[1].Dummy); + } + } + + } + +} // namespace Nuclex.Support.Serialization + +#endif // UNITTEST \ No newline at end of file diff --git a/Source/Serialization/BinarySerializer.cs b/Source/Serialization/BinarySerializer.cs new file mode 100644 index 0000000..179db38 --- /dev/null +++ b/Source/Serialization/BinarySerializer.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +using Nuclex.Support.Serialization; + +namespace Nuclex.Support.Serialization { + + /// Utility class for serialization objects into binary data + /// Data type to be serialized + public static class BinarySerializer + where BinarySerializableType : IBinarySerializable { + + /// Serializes a collection of binary serializable objects + /// Collection to be serialized + /// BinaryWriter to serialize the collection into + public static void SaveCollection( + ICollection collection, BinaryWriter writer + ) { + + // Save the file format version so the loading routine can detect + // which version of the file format has to be loaded + writer.Write((int)1); + + // Serialize all the blueprints in the collection + writer.Write((int)collection.Count); + foreach(BinarySerializableType item in collection) { + + // Save the type name of the object so we can recreate it later + writer.Write(item.GetType().AssemblyQualifiedName); + + // Let the object save its own data + ((IBinarySerializable)item).Save(writer); + + } // foreach + + } + + /// Loads a collection from its serialized representation + /// Collection to be deserialized into + /// Reader to use for reading the collection + public static void LoadCollection( + ICollection collection, BinaryReader reader + ) { + + // Read and verify the version of the file format this was saved in + int version = reader.ReadInt32(); + if(version > 1) + throw new InvalidOperationException("File format is too new"); + + // Read all the serialized blueprints + int count = reader.ReadInt32(); + for(int index = 0; index < count; ++index) { + + // Try to create an instance from the serialized type name + BinarySerializableType item = (BinarySerializableType)Activator.CreateInstance( + Type.GetType(reader.ReadString()) + ); + + // Let the blueprint load its own data and add it to the collection + ((IBinarySerializable)item).Load(reader); + collection.Add(item); + + } // for + + } + + } // class BinarySerializer + +} // namespace Nuclex.Support.Serialization diff --git a/Source/Serialization/IBinarySerializable.cs b/Source/Serialization/IBinarySerializable.cs new file mode 100644 index 0000000..c8ff23e --- /dev/null +++ b/Source/Serialization/IBinarySerializable.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Nuclex.Support.Serialization { + + /// Interface for objects able to serialize themselfes into a binary format + /// + /// Sometimes, the limitations of XML serialization are too strict, especially + /// in the context of a game where you might need to serialize larger chunks of + /// binary data or in cases where you do not wish to expose a default constructor + /// in your classes. This interface defines two simple methods that can be + /// used to load and save an object's state in a simple manner. + /// + public interface IBinarySerializable { + + /// Loads the object's state from its serialized representation + /// Reader to use for reading the object's state + void Load(BinaryReader reader); + + /// Save the object's state into a serialized representation + /// Writer to use for writing the object's state + void Save(BinaryWriter writer); + + } + +} // namespace Nuclex.Support.Serialization