diff --git a/DLS2/Chunks/Chunk.cs b/DLS2/Chunks/Chunk.cs
new file mode 100644
index 0000000..6b4b775
--- /dev/null
+++ b/DLS2/Chunks/Chunk.cs
@@ -0,0 +1,98 @@
+using Kermalis.EndianBinaryIO;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ public abstract class DLSChunk
+ {
+ /// Length 4
+ public string ChunkName { get; }
+ /// Size in bytes
+ protected internal uint Size { get; protected set; }
+
+ protected DLSChunk(string name)
+ {
+ ChunkName = name;
+ }
+ protected DLSChunk(string name, EndianBinaryReader reader)
+ {
+ ChunkName = name;
+ Size = reader.ReadUInt32();
+ }
+
+ protected long GetEndOffset(EndianBinaryReader reader)
+ {
+ return reader.BaseStream.Position + Size;
+ }
+ protected void EatRemainingBytes(EndianBinaryReader reader, long endOffset)
+ {
+ if (reader.BaseStream.Position > endOffset)
+ {
+ throw new InvalidDataException();
+ }
+ reader.BaseStream.Position = endOffset;
+ }
+
+ internal abstract void UpdateSize();
+
+ internal virtual void Write(EndianBinaryWriter writer)
+ {
+ UpdateSize();
+ writer.Write(ChunkName, 4);
+ writer.Write(Size);
+ }
+
+ internal static List GetAllChunks(EndianBinaryReader reader, long endOffset)
+ {
+ var chunks = new List();
+ while (reader.BaseStream.Position < endOffset)
+ {
+ chunks.Add(SwitchNextChunk(reader));
+ }
+ if (reader.BaseStream.Position > endOffset)
+ {
+ throw new InvalidDataException();
+ }
+ return chunks;
+ }
+ private static DLSChunk SwitchNextChunk(EndianBinaryReader reader)
+ {
+ string str = reader.ReadString(4, false);
+ switch (str)
+ {
+ case "art1": return new Level1ArticulatorChunk(reader);
+ case "art2": return new Level2ArticulatorChunk(reader);
+ case "colh": return new CollectionHeaderChunk(reader);
+ case "data": return new DataChunk(reader);
+ case "dlid": return new DLSIDChunk(reader);
+ case "fmt ": return new FormatChunk(reader);
+ case "insh": return new InstrumentHeaderChunk(reader);
+ case "LIST": return new ListChunk(reader);
+ case "ptbl": return new PoolTableChunk(reader);
+ case "rgnh": return new RegionHeaderChunk(reader);
+ case "wlnk": return new WaveLinkChunk(reader);
+ case "wsmp": return new WaveSampleChunk(reader);
+ // InfoSubChunks
+ case "IARL":
+ case "IART":
+ case "ICMS":
+ case "ICMD":
+ case "ICOP":
+ case "ICRD":
+ case "IENG":
+ case "IGNR":
+ case "IKEY":
+ case "IMED":
+ case "INAM":
+ case "IPRD":
+ case "ISBJ":
+ case "ISFT":
+ case "ISRC":
+ case "ISRF":
+ case "ITCH": return new InfoSubChunk(str, reader);
+ default: return new UnsupportedChunk(str, reader);
+ }
+ }
+ }
+}
diff --git a/DLS2/Chunks/CollectionHeaderChunk.cs b/DLS2/Chunks/CollectionHeaderChunk.cs
new file mode 100644
index 0000000..0f5256e
--- /dev/null
+++ b/DLS2/Chunks/CollectionHeaderChunk.cs
@@ -0,0 +1,29 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ // Collection Header Chunk - Page 40 of spec
+ public sealed class CollectionHeaderChunk : DLSChunk
+ {
+ public uint NumInstruments { get; internal set; }
+
+ internal CollectionHeaderChunk() : base("colh") { }
+ public CollectionHeaderChunk(EndianBinaryReader reader) : base("colh", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ NumInstruments = reader.ReadUInt32();
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4; // NumInstruments
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(NumInstruments);
+ }
+ }
+}
diff --git a/DLS2/Chunks/ConditionalChunk.cs b/DLS2/Chunks/ConditionalChunk.cs
new file mode 100644
index 0000000..c96c20e
--- /dev/null
+++ b/DLS2/Chunks/ConditionalChunk.cs
@@ -0,0 +1,36 @@
+namespace Kermalis.DLS2
+{
+ // ALL TODO:
+
+ /*public enum DLSConditional : ushort
+ {
+ And = 1,
+ Or = 2,
+ Xor = 3,
+ Add = 4,
+ Subtract = 5,
+ Multiply = 6,
+ Divide = 7,
+ LogicalAnd = 8,
+ LogicalOr = 9,
+ Lt = 10,
+ Le = 11,
+ Gt = 12,
+ Ge = 13,
+ Eq = 14,
+ Not = 15,
+ Const = 16,
+ Query = 17,
+ QuerySupported = 18
+ }
+
+ // Conditional Chunk - Page 42 of spec
+ internal sealed class ConditionalChunk : DLSChunk
+ {
+ public ConditionalChunk(EndianBinaryReader reader) : base("cdl ", reader)
+ {
+ DLSConditional cond = reader.ReadEnum();
+
+ }
+ }*/
+}
diff --git a/DLS2/Chunks/DLSIDChunk.cs b/DLS2/Chunks/DLSIDChunk.cs
new file mode 100644
index 0000000..b9b8aab
--- /dev/null
+++ b/DLS2/Chunks/DLSIDChunk.cs
@@ -0,0 +1,45 @@
+using Kermalis.EndianBinaryIO;
+using System;
+
+namespace Kermalis.DLS2
+{
+ // DLSID Chunk - Page 40 of spec
+ public sealed class DLSIDChunk : DLSChunk
+ {
+ private DLSID _dlsid;
+ public DLSID DLSID
+ {
+ get => _dlsid;
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _dlsid = value;
+ }
+ }
+
+ public DLSIDChunk(DLSID id) : base("dlid")
+ {
+ DLSID = id;
+ }
+ public DLSIDChunk(EndianBinaryReader reader) : base("dlid", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ DLSID = new DLSID(reader);
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 16; // DLSID
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ DLSID.Write(writer);
+ }
+ }
+}
diff --git a/DLS2/Chunks/DataChunk.cs b/DLS2/Chunks/DataChunk.cs
new file mode 100644
index 0000000..f924db3
--- /dev/null
+++ b/DLS2/Chunks/DataChunk.cs
@@ -0,0 +1,10 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class DataChunk : RawDataChunk
+ {
+ public DataChunk(byte[] data) : base("data", data) { }
+ internal DataChunk(EndianBinaryReader reader) : base("data", reader) { }
+ }
+}
diff --git a/DLS2/Chunks/FormatChunk.cs b/DLS2/Chunks/FormatChunk.cs
new file mode 100644
index 0000000..7a7779e
--- /dev/null
+++ b/DLS2/Chunks/FormatChunk.cs
@@ -0,0 +1,105 @@
+using Kermalis.EndianBinaryIO;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ public abstract class FormatInfo
+ {
+ public ushort BitsPerSample { get; set; }
+
+ internal abstract void Write(EndianBinaryWriter writer);
+ }
+ public sealed class PCMInfo : FormatInfo
+ {
+ internal PCMInfo() { }
+ internal PCMInfo(EndianBinaryReader reader)
+ {
+ BitsPerSample = reader.ReadUInt16();
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(BitsPerSample);
+ }
+ }
+ // Untested!
+ public sealed class ExtensibleInfo : FormatInfo
+ {
+ public ushort ExtraInfo { get; set; }
+ public uint ChannelMask { get; set; }
+ public DLSID SubFormat { get; set; }
+
+ internal ExtensibleInfo()
+ {
+ SubFormat = new DLSID();
+ }
+ internal ExtensibleInfo(EndianBinaryReader reader)
+ {
+ BitsPerSample = reader.ReadUInt16();
+ ushort byteSize = reader.ReadUInt16();
+ if (byteSize != 22)
+ {
+ throw new InvalidDataException();
+ }
+ ExtraInfo = reader.ReadUInt16();
+ ChannelMask = reader.ReadUInt32();
+ SubFormat = new DLSID(reader);
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(BitsPerSample);
+ writer.Write(22u);
+ writer.Write(ExtraInfo);
+ writer.Write(ChannelMask);
+ SubFormat.Write(writer);
+ }
+ }
+
+ // Format Chunk - Page 57 of spec
+ public sealed class FormatChunk : DLSChunk
+ {
+ public WaveInfo WaveInfo { get; }
+ public FormatInfo FormatInfo { get; }
+
+ public FormatChunk(WaveFormat format) : base("fmt ")
+ {
+ WaveInfo = new WaveInfo() { FormatTag = format };
+ if (format == WaveFormat.Extensible)
+ {
+ FormatInfo = new ExtensibleInfo();
+ }
+ else
+ {
+ FormatInfo = new PCMInfo();
+ }
+ }
+ internal FormatChunk(EndianBinaryReader reader) : base("fmt ", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ WaveInfo = new WaveInfo(reader);
+ if (WaveInfo.FormatTag == WaveFormat.Extensible)
+ {
+ FormatInfo = new ExtensibleInfo(reader);
+ }
+ else
+ {
+ FormatInfo = new PCMInfo(reader);
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 14 // WaveFormat
+ + (WaveInfo.FormatTag == DLS2.WaveFormat.Extensible ? 26u : 2u); // FormatInfo
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ WaveInfo.Write(writer);
+ FormatInfo.Write(writer);
+ }
+ }
+}
diff --git a/DLS2/Chunks/InfoSubChunk.cs b/DLS2/Chunks/InfoSubChunk.cs
new file mode 100644
index 0000000..71a629a
--- /dev/null
+++ b/DLS2/Chunks/InfoSubChunk.cs
@@ -0,0 +1,53 @@
+using Kermalis.EndianBinaryIO;
+using System;
+using System.Linq;
+
+namespace Kermalis.DLS2
+{
+ public sealed class InfoSubChunk : DLSChunk
+ {
+ private string _text;
+ public string Text
+ {
+ get => _text;
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ if (value.Any(c => c > sbyte.MaxValue))
+ {
+ throw new ArgumentException("Text must be ASCII");
+ }
+ _text = value;
+ }
+ }
+
+ public InfoSubChunk(string name, string text) : base(name)
+ {
+ Text = text;
+ }
+ internal InfoSubChunk(string name, EndianBinaryReader reader) : base(name, reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ _text = reader.ReadStringNullTerminated();
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = (uint)_text.Length + 1; // +1 for \0
+ if (Size % 2 != 0) // Align by 2 bytes
+ {
+ Size++;
+ }
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(_text, (int)Size);
+ }
+ }
+}
diff --git a/DLS2/Chunks/InstrumentHeaderChunk.cs b/DLS2/Chunks/InstrumentHeaderChunk.cs
new file mode 100644
index 0000000..077070c
--- /dev/null
+++ b/DLS2/Chunks/InstrumentHeaderChunk.cs
@@ -0,0 +1,46 @@
+using Kermalis.EndianBinaryIO;
+using System;
+
+namespace Kermalis.DLS2
+{
+ // Instrument Header Chunk - Page 45 of spec
+ public sealed class InstrumentHeaderChunk : DLSChunk
+ {
+ public uint NumRegions { get; set; }
+ private MIDILocale _locale;
+ public MIDILocale Locale
+ {
+ get => _locale;
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _locale = value;
+ }
+ }
+
+ public InstrumentHeaderChunk() : base("insh") { }
+ internal InstrumentHeaderChunk(EndianBinaryReader reader) : base("insh", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ NumRegions = reader.ReadUInt32();
+ _locale = new MIDILocale(reader);
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // NumRegions
+ + 8; // Locale
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(NumRegions);
+ _locale.Write(writer);
+ }
+ }
+}
diff --git a/DLS2/Chunks/Level1ArticulatorChunk.cs b/DLS2/Chunks/Level1ArticulatorChunk.cs
new file mode 100644
index 0000000..c6b4f26
--- /dev/null
+++ b/DLS2/Chunks/Level1ArticulatorChunk.cs
@@ -0,0 +1,119 @@
+using Kermalis.EndianBinaryIO;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ // Level 1 Articulator Chunk - Page 46 of spec
+ public sealed class Level1ArticulatorChunk : DLSChunk, IList, IReadOnlyList
+ {
+ private readonly List _connectionBlocks;
+
+ public Level1ArticulatorConnectionBlock this[int index]
+ {
+ get => _connectionBlocks[index];
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _connectionBlocks[index] = value;
+ }
+ }
+ public int Count => _connectionBlocks.Count;
+ public bool IsReadOnly => false;
+
+ public Level1ArticulatorChunk() : base("art1")
+ {
+ _connectionBlocks = new List();
+ }
+ internal Level1ArticulatorChunk(EndianBinaryReader reader) : base("art1", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ uint byteSize = reader.ReadUInt32();
+ if (byteSize != 8)
+ {
+ throw new InvalidDataException();
+ }
+ uint numConnectionBlocks = reader.ReadUInt32();
+ _connectionBlocks = new List((int)numConnectionBlocks);
+ for (uint i = 0; i < numConnectionBlocks; i++)
+ {
+ _connectionBlocks.Add(new Level1ArticulatorConnectionBlock(reader));
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // byteSize
+ + 4 // _numConnectionBlocks
+ + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(8u);
+ writer.Write((uint)_connectionBlocks.Count);
+ for (int i = 0; i < _connectionBlocks.Count; i++)
+ {
+ _connectionBlocks[i].Write(writer);
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _connectionBlocks.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _connectionBlocks.GetEnumerator();
+ }
+
+ public void Add(Level1ArticulatorConnectionBlock item)
+ {
+ if (item is null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+ _connectionBlocks.Add(item);
+ }
+ public void Clear()
+ {
+ _connectionBlocks.Clear();
+ }
+ public void CopyTo(Level1ArticulatorConnectionBlock[] array, int arrayIndex)
+ {
+ _connectionBlocks.CopyTo(array, arrayIndex);
+ }
+ public bool Contains(Level1ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.Contains(item);
+ }
+ public int IndexOf(Level1ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.IndexOf(item);
+ }
+ public void Insert(int index, Level1ArticulatorConnectionBlock item)
+ {
+ if (item is null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+ _connectionBlocks.Insert(index, item);
+ }
+ public bool Remove(Level1ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.Remove(item);
+ }
+ public void RemoveAt(int index)
+ {
+ _connectionBlocks.RemoveAt(index);
+ }
+
+ }
+}
diff --git a/DLS2/Chunks/Level2ArticulatorChunk.cs b/DLS2/Chunks/Level2ArticulatorChunk.cs
new file mode 100644
index 0000000..71e8b33
--- /dev/null
+++ b/DLS2/Chunks/Level2ArticulatorChunk.cs
@@ -0,0 +1,119 @@
+using Kermalis.EndianBinaryIO;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ // Level 2 Articulator Chunk - Page 49 of spec
+ public sealed class Level2ArticulatorChunk : DLSChunk, IList, IReadOnlyList
+ {
+ private readonly List _connectionBlocks;
+
+ public Level2ArticulatorConnectionBlock this[int index]
+ {
+ get => _connectionBlocks[index];
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _connectionBlocks[index] = value;
+ }
+ }
+ public int Count => _connectionBlocks.Count;
+ public bool IsReadOnly => false;
+
+ public Level2ArticulatorChunk() : base("art2")
+ {
+ _connectionBlocks = new List();
+ }
+ internal Level2ArticulatorChunk(EndianBinaryReader reader) : base("art2", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ uint byteSize = reader.ReadUInt32();
+ if (byteSize != 8)
+ {
+ throw new InvalidDataException();
+ }
+ uint numConnectionBlocks = reader.ReadUInt32();
+ _connectionBlocks = new List((int)numConnectionBlocks);
+ for (uint i = 0; i < numConnectionBlocks; i++)
+ {
+ _connectionBlocks.Add(new Level2ArticulatorConnectionBlock(reader));
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // byteSize
+ + 4 // _numConnectionBlocks
+ + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(8u);
+ writer.Write((uint)_connectionBlocks.Count);
+ for (int i = 0; i < _connectionBlocks.Count; i++)
+ {
+ _connectionBlocks[i].Write(writer);
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _connectionBlocks.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _connectionBlocks.GetEnumerator();
+ }
+
+ public void Add(Level2ArticulatorConnectionBlock item)
+ {
+ if (item is null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+ _connectionBlocks.Add(item);
+ }
+ public void Clear()
+ {
+ _connectionBlocks.Clear();
+ }
+ public void CopyTo(Level2ArticulatorConnectionBlock[] array, int arrayIndex)
+ {
+ _connectionBlocks.CopyTo(array, arrayIndex);
+ }
+ public bool Contains(Level2ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.Contains(item);
+ }
+ public int IndexOf(Level2ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.IndexOf(item);
+ }
+ public void Insert(int index, Level2ArticulatorConnectionBlock item)
+ {
+ if (item is null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+ _connectionBlocks.Insert(index, item);
+ }
+ public bool Remove(Level2ArticulatorConnectionBlock item)
+ {
+ return _connectionBlocks.Remove(item);
+ }
+ public void RemoveAt(int index)
+ {
+ _connectionBlocks.RemoveAt(index);
+ }
+
+ }
+}
diff --git a/DLS2/Chunks/ListChunk.cs b/DLS2/Chunks/ListChunk.cs
new file mode 100644
index 0000000..1c4107e
--- /dev/null
+++ b/DLS2/Chunks/ListChunk.cs
@@ -0,0 +1,112 @@
+using Kermalis.EndianBinaryIO;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Kermalis.DLS2
+{
+ // LIST Chunk - Page 40 of spec
+ public sealed class ListChunk : DLSChunk, IList, IReadOnlyList
+ {
+ /// Length 4
+ public string Identifier { get; set; }
+ private readonly List _children;
+
+ public int Count => _children.Count;
+ public bool IsReadOnly => false;
+ public DLSChunk this[int index]
+ {
+ get => _children[index];
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _children[index] = value;
+ }
+ }
+
+ public ListChunk(string identifier) : base("LIST")
+ {
+ Identifier = identifier;
+ _children = new List();
+ }
+ internal ListChunk(EndianBinaryReader reader) : base("LIST", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ Identifier = reader.ReadString(4, false);
+ _children = GetAllChunks(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4; // Identifier
+ foreach (DLSChunk c in _children)
+ {
+ c.UpdateSize();
+ Size += c.Size + 8;
+ }
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(Identifier, 4);
+ foreach (DLSChunk c in _children)
+ {
+ c.Write(writer);
+ }
+ }
+
+ public void Add(DLSChunk chunk)
+ {
+ if (chunk is null)
+ {
+ throw new ArgumentNullException(nameof(chunk));
+ }
+ _children.Add(chunk);
+ }
+ public void Clear()
+ {
+ _children.Clear();
+ }
+ public bool Contains(DLSChunk chunk)
+ {
+ return _children.Contains(chunk);
+ }
+ public void CopyTo(DLSChunk[] array, int arrayIndex)
+ {
+ _children.CopyTo(array, arrayIndex);
+ }
+ public int IndexOf(DLSChunk chunk)
+ {
+ return _children.IndexOf(chunk);
+ }
+ public void Insert(int index, DLSChunk chunk)
+ {
+ if (chunk is null)
+ {
+ throw new ArgumentNullException(nameof(chunk));
+ }
+ _children.Insert(index, chunk);
+ }
+ public bool Remove(DLSChunk chunk)
+ {
+ return _children.Remove(chunk);
+ }
+ public void RemoveAt(int index)
+ {
+ _children.RemoveAt(index);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _children.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _children.GetEnumerator();
+ }
+ }
+}
diff --git a/DLS2/Chunks/PoolTableChunk.cs b/DLS2/Chunks/PoolTableChunk.cs
new file mode 100644
index 0000000..07d698c
--- /dev/null
+++ b/DLS2/Chunks/PoolTableChunk.cs
@@ -0,0 +1,71 @@
+using Kermalis.EndianBinaryIO;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ // Pool Table Chunk - Page 54 of spec
+ public sealed class PoolTableChunk : DLSChunk, IReadOnlyList
+ {
+ private uint _numCues;
+ private List _poolCues;
+
+ public uint this[int index] => _poolCues[index];
+ public int Count => (int)_numCues;
+
+ internal PoolTableChunk() : base("ptbl")
+ {
+ _poolCues = new List();
+ }
+ internal PoolTableChunk(EndianBinaryReader reader) : base("ptbl", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ uint byteSize = reader.ReadUInt32();
+ if (byteSize != 8)
+ {
+ throw new InvalidDataException();
+ }
+ _numCues = reader.ReadUInt32();
+ _poolCues = new List((int)_numCues);
+ for (uint i = 0; i < _numCues; i++)
+ {
+ _poolCues.Add(reader.ReadUInt32());
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal void UpdateCues(List newCues)
+ {
+ _numCues = (uint)newCues.Count;
+ _poolCues = newCues;
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // byteSize
+ + 4 // _numCues
+ + (4 * _numCues); // _poolCues
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(8u);
+ writer.Write(_numCues);
+ for (int i = 0; i < _numCues; i++)
+ {
+ writer.Write(_poolCues[i]);
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _poolCues.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _poolCues.GetEnumerator();
+ }
+ }
+}
diff --git a/DLS2/Chunks/RawDataChunk.cs b/DLS2/Chunks/RawDataChunk.cs
new file mode 100644
index 0000000..117d24f
--- /dev/null
+++ b/DLS2/Chunks/RawDataChunk.cs
@@ -0,0 +1,50 @@
+using Kermalis.EndianBinaryIO;
+using System;
+
+namespace Kermalis.DLS2
+{
+ public abstract class RawDataChunk : DLSChunk
+ {
+ private byte[] _data;
+ public byte[] Data
+ {
+ get => _data;
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _data = value;
+ }
+ }
+
+ protected RawDataChunk(string name, byte[] data) : base(name)
+ {
+ Data = data;
+ }
+ protected RawDataChunk(string name, EndianBinaryReader reader) : base(name, reader)
+ {
+ _data = reader.ReadBytes((int)Size);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = (uint)_data.Length;
+ if (Size % 2 != 0) // Align by 2 bytes
+ {
+ Size++;
+ }
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(_data);
+ for (int i = _data.Length; i < Size; i++)
+ {
+ writer.Write((byte)0);
+ }
+ }
+ }
+}
diff --git a/DLS2/Chunks/RegionHeaderChunk.cs b/DLS2/Chunks/RegionHeaderChunk.cs
new file mode 100644
index 0000000..aaabd0b
--- /dev/null
+++ b/DLS2/Chunks/RegionHeaderChunk.cs
@@ -0,0 +1,52 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ // Region Header Chunk - Page 45 of spec
+ public sealed class RegionHeaderChunk : DLSChunk
+ {
+ public Range KeyRange { get; set; }
+ public Range VelocityRange { get; set; }
+ public ushort Options { get; set; }
+ public ushort KeyGroup { get; set; }
+ public ushort Layer { get; set; }
+
+ public RegionHeaderChunk() : base("rgnh")
+ {
+ KeyRange = new Range(0, 127);
+ VelocityRange = new Range(0, 127);
+ }
+ internal RegionHeaderChunk(EndianBinaryReader reader) : base("rgnh", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ KeyRange = new Range(reader);
+ VelocityRange = new Range(reader);
+ Options = reader.ReadUInt16();
+ KeyGroup = reader.ReadUInt16();
+ if (Size >= 14) // Size of 12 is also valid
+ {
+ Layer = reader.ReadUInt16();
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // KeyRange
+ + 4 // VelocityRange
+ + 2 // Options
+ + 2 // KeyGroup
+ + 2; // Layer
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ KeyRange.Write(writer);
+ VelocityRange.Write(writer);
+ writer.Write(Options);
+ writer.Write(KeyGroup);
+ writer.Write(Layer);
+ }
+ }
+}
diff --git a/DLS2/Chunks/UnsupportedChunk.cs b/DLS2/Chunks/UnsupportedChunk.cs
new file mode 100644
index 0000000..acdccb1
--- /dev/null
+++ b/DLS2/Chunks/UnsupportedChunk.cs
@@ -0,0 +1,10 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class UnsupportedChunk : RawDataChunk
+ {
+ public UnsupportedChunk(string name, byte[] data) : base(name, data) { }
+ internal UnsupportedChunk(string name, EndianBinaryReader reader) : base(name, reader) { }
+ }
+}
diff --git a/DLS2/Chunks/VersionChunk.cs b/DLS2/Chunks/VersionChunk.cs
new file mode 100644
index 0000000..42302bf
--- /dev/null
+++ b/DLS2/Chunks/VersionChunk.cs
@@ -0,0 +1,14 @@
+namespace Kermalis.DLS2
+{
+ // TODO:
+
+ /*public sealed class VersionChunk : DLSChunk
+ {
+
+
+ internal VersionChunk(EndianBinaryReader reader) : base("vers", reader)
+ {
+
+ }
+ }*/
+}
diff --git a/DLS2/Chunks/WaveLinkChunk.cs b/DLS2/Chunks/WaveLinkChunk.cs
new file mode 100644
index 0000000..37cb660
--- /dev/null
+++ b/DLS2/Chunks/WaveLinkChunk.cs
@@ -0,0 +1,43 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class WaveLinkChunk : DLSChunk
+ {
+ public WaveLinkOptions Options { get; set; }
+ public ushort PhaseGroup { get; set; }
+ public WaveLinkChannels Channels { get; set; }
+ public uint TableIndex { get; set; }
+
+ public WaveLinkChunk() : base("wlnk")
+ {
+ Channels = WaveLinkChannels.Left;
+ }
+ internal WaveLinkChunk(EndianBinaryReader reader) : base("wlnk", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ Options = reader.ReadEnum();
+ PhaseGroup = reader.ReadUInt16();
+ Channels = reader.ReadEnum();
+ TableIndex = reader.ReadUInt32();
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 2 // Options
+ + 2 // PhaseGroup
+ + 4 // Channel
+ + 4; // TableIndex
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(Options);
+ writer.Write(PhaseGroup);
+ writer.Write(Channels);
+ writer.Write(TableIndex);
+ }
+ }
+}
diff --git a/DLS2/Chunks/WaveSampleChunk.cs b/DLS2/Chunks/WaveSampleChunk.cs
new file mode 100644
index 0000000..d005392
--- /dev/null
+++ b/DLS2/Chunks/WaveSampleChunk.cs
@@ -0,0 +1,69 @@
+using Kermalis.EndianBinaryIO;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class WaveSampleChunk : DLSChunk
+ {
+ public ushort UnityNote { get; set; }
+ public short FineTune { get; set; }
+ public int Gain { get; set; }
+ public WaveSampleOptions Options { get; set; }
+
+ public WaveSampleLoop Loop { get; set; } // Combining "SampleLoops" and the loop list
+
+ public WaveSampleChunk() : base("wsmp")
+ {
+ UnityNote = 60;
+ Loop = null;
+ }
+ internal WaveSampleChunk(EndianBinaryReader reader) : base("wsmp", reader)
+ {
+ long endOffset = GetEndOffset(reader);
+ uint byteSize = reader.ReadUInt32();
+ if (byteSize != 20)
+ {
+ throw new InvalidDataException();
+ }
+ UnityNote = reader.ReadUInt16();
+ FineTune = reader.ReadInt16();
+ Gain = reader.ReadInt32();
+ Options = reader.ReadEnum();
+ if (reader.ReadUInt32() == 1)
+ {
+ Loop = new WaveSampleLoop(reader);
+ }
+ EatRemainingBytes(reader, endOffset);
+ }
+
+ internal override void UpdateSize()
+ {
+ Size = 4 // byteSize
+ + 2 // UnityNote
+ + 2 // FineTune
+ + 4 // Gain
+ + 4 // Options
+ + 4 // DoesLoop
+ + (Loop is null ? 0u : 16u); // Loop
+ }
+
+ internal override void Write(EndianBinaryWriter writer)
+ {
+ base.Write(writer);
+ writer.Write(20u);
+ writer.Write(UnityNote);
+ writer.Write(FineTune);
+ writer.Write(Gain);
+ writer.Write(Options);
+ if (Loop is null)
+ {
+ writer.Write(0u);
+ }
+ else
+ {
+ writer.Write(1u);
+ Loop.Write(writer);
+ }
+ }
+ }
+}
diff --git a/DLS2/DLS.cs b/DLS2/DLS.cs
new file mode 100644
index 0000000..d785aca
--- /dev/null
+++ b/DLS2/DLS.cs
@@ -0,0 +1,235 @@
+using Kermalis.EndianBinaryIO;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Kermalis.DLS2
+{
+ public sealed class DLS : IList, IReadOnlyList
+ {
+ private readonly List _chunks;
+
+ public int Count => _chunks.Count;
+ public bool IsReadOnly => false;
+ public DLSChunk this[int index]
+ {
+ get => _chunks[index];
+ set
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ _chunks[index] = value;
+ }
+ }
+
+ public CollectionHeaderChunk CollectionHeader => GetChunk();
+ public ListChunk InstrumentList => GetListChunk("lins");
+ public PoolTableChunk PoolTable => GetChunk();
+ public ListChunk WavePool => GetListChunk("wvpl");
+
+ private T GetChunk() where T : DLSChunk
+ {
+ return (T)_chunks.Find(c => c is T);
+ }
+ private ListChunk GetListChunk(string str)
+ {
+ return (ListChunk)_chunks.Find(c => c is ListChunk lc && lc.Identifier == str);
+ }
+
+#if DEBUG
+ public static void Main()
+ {
+ //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test.dls");
+ //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test2.dls");
+ //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Arachno SoundFont - Version 1.0.dls");
+ //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Musyng Kite.dls");
+ new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\RSE Corrected Soundfont Revision 17.dls");
+ }
+#endif
+
+ /// For creating.
+ public DLS()
+ {
+ _chunks = new List()
+ {
+ new CollectionHeaderChunk(),
+ new ListChunk("lins"),
+ new PoolTableChunk(),
+ new ListChunk("wvpl"),
+ };
+ }
+ public DLS(string path)
+ {
+ var reader = new EndianBinaryReader(File.Open(path, FileMode.Open));
+ {
+ _chunks = Init(reader);
+ }
+ }
+ public DLS(Stream stream)
+ {
+ _chunks = Init(new EndianBinaryReader(stream));
+ }
+ private List Init(EndianBinaryReader reader)
+ {
+ string str = reader.ReadString(4, false);
+ if (str != "RIFF")
+ {
+ throw new InvalidDataException("RIFF header was not found at the start of the file.");
+ }
+ uint size = reader.ReadUInt32();
+ long endOffset = reader.BaseStream.Position + size;
+ str = reader.ReadString(4, false);
+ if (str != "DLS ")
+ {
+ throw new InvalidDataException("DLS header was not found at the expected offset.");
+ }
+ return DLSChunk.GetAllChunks(reader, endOffset);
+ }
+
+ public void UpdateCollectionHeader()
+ {
+ CollectionHeader.NumInstruments = (uint)InstrumentList.Count;
+ }
+ /// Updates the pointers in the . Should be called after modifying .
+ public void UpdatePoolTable()
+ {
+ ListChunk wvpl = WavePool;
+ var newCues = new List(wvpl.Count);
+ uint cur = 0;
+ for (int i = 0; i < wvpl.Count; i++)
+ {
+ newCues.Add(cur);
+ DLSChunk c = wvpl[i];
+ c.UpdateSize();
+ cur += c.Size + 8;
+ }
+ PoolTable.UpdateCues(newCues);
+ }
+ public void Save(string path)
+ {
+ UpdateCollectionHeader();
+ UpdatePoolTable();
+
+ var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create));
+ {
+ writer.Write("RIFF", 4);
+ writer.Write(UpdateSize());
+ writer.Write("DLS ", 4);
+ foreach (DLSChunk c in _chunks)
+ {
+ c.Write(writer);
+ }
+ }
+ }
+
+ public string GetHierarchy()
+ {
+ var str = new StringBuilder();
+ int tabLevel = 0;
+ void ApplyTabLevel()
+ {
+ for (int t = 0; t < tabLevel; t++)
+ {
+ str.Append('\t');
+ }
+ }
+ void Recursion(IReadOnlyList parent, string listName)
+ {
+ ApplyTabLevel();
+ str.Append($"{listName} ({parent.Count})");
+ tabLevel++;
+ foreach (DLSChunk c in parent)
+ {
+ str.AppendLine();
+ if (c is ListChunk lc)
+ {
+ Recursion(lc, $"{lc.ChunkName} '{lc.Identifier}'");
+ }
+ else
+ {
+ ApplyTabLevel();
+ str.Append($"<{c.ChunkName}>");
+ if (c is InfoSubChunk ic)
+ {
+ str.Append($" [\"{ic.Text}\"]");
+ }
+ else if (c is RawDataChunk dc)
+ {
+ str.Append($" [{dc.Data.Length} bytes]");
+ }
+ }
+ }
+#pragma warning disable IDE0059 // Unnecessary assignment of a value
+ tabLevel--;
+#pragma warning restore IDE0059 // Unnecessary assignment of a value
+ }
+ Recursion(this, "RIFF 'DLS '");
+ return str.ToString();
+ }
+
+ private uint UpdateSize()
+ {
+ uint size = 4;
+ foreach (DLSChunk c in _chunks)
+ {
+ c.UpdateSize();
+ size += c.Size + 8;
+ }
+ return size;
+ }
+
+ public void Add(DLSChunk chunk)
+ {
+ if (chunk is null)
+ {
+ throw new ArgumentNullException(nameof(chunk));
+ }
+ _chunks.Add(chunk);
+ }
+ public void Clear()
+ {
+ _chunks.Clear();
+ }
+ public bool Contains(DLSChunk chunk)
+ {
+ return _chunks.Contains(chunk);
+ }
+ public void CopyTo(DLSChunk[] array, int arrayIndex)
+ {
+ _chunks.CopyTo(array, arrayIndex);
+ }
+ public int IndexOf(DLSChunk chunk)
+ {
+ return _chunks.IndexOf(chunk);
+ }
+ public void Insert(int index, DLSChunk chunk)
+ {
+ if (chunk is null)
+ {
+ throw new ArgumentNullException(nameof(chunk));
+ }
+ _chunks.Insert(index, chunk);
+ }
+ public bool Remove(DLSChunk chunk)
+ {
+ return _chunks.Remove(chunk);
+ }
+ public void RemoveAt(int index)
+ {
+ _chunks.RemoveAt(index);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _chunks.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _chunks.GetEnumerator();
+ }
+ }
+}
diff --git a/DLS2/DLS2.csproj b/DLS2/DLS2.csproj
new file mode 100644
index 0000000..4da739f
--- /dev/null
+++ b/DLS2/DLS2.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Kermalis
+
+ DLS2
+ DLS2
+ DLS2
+ Kermalis.DLS2
+ 1.0.0.0
+ ..\Build
+
+
+
+ netcoreapp3.1
+ Exe
+
+
+
+ netcoreapp3.1
+ Auto
+ none
+ false
+
+
+
+
+
+
+
diff --git a/DLS2/Enums/Level1ArticulatorEnums.cs b/DLS2/Enums/Level1ArticulatorEnums.cs
new file mode 100644
index 0000000..0cbe822
--- /dev/null
+++ b/DLS2/Enums/Level1ArticulatorEnums.cs
@@ -0,0 +1,44 @@
+namespace Kermalis.DLS2
+{
+ public enum Level1ArticulatorSource : ushort
+ {
+ None = 0x0,
+ LFO = 0x1,
+ KeyOnVelocity = 0x2,
+ KeyNumber = 0x3,
+ EG1 = 0x4,
+ EG2 = 0x5,
+ PitchWheel = 0x6,
+ Modulation_CC1 = 0x81,
+ ChannelVolume_CC7 = 0x87,
+ Pan_CC10 = 0x8A,
+ Expression_CC11 = 0x8B,
+ PitchBendRange_RPN0 = 0x100,
+ FineTune_RPN1 = 0x101,
+ CoarseTune_RPN2 = 0x102
+ }
+
+ public enum Level1ArticulatorDestination : ushort
+ {
+ None = 0x0,
+ Gain = 0x1,
+ Pitch = 0x3,
+ Pan = 0x4,
+ LFOFrequency = 0x104,
+ LFOStartDelay = 0x105,
+ EG1AttackTime = 0x206,
+ EG1DecayTime = 0x207,
+ EG1ReleaseTime = 0x209,
+ EG1SustainLevel = 0x20A,
+ EG2AttackTime = 0x30A,
+ EG2DecayTime = 0x30B,
+ EG2ReleaseTime = 0x30D,
+ EG2SustainLevel = 0x30E
+ }
+
+ public enum Level1ArticulatorTransform : byte
+ {
+ None = 0x0,
+ Concave = 0x1
+ }
+}
diff --git a/DLS2/Enums/Level2ArticulatorEnums.cs b/DLS2/Enums/Level2ArticulatorEnums.cs
new file mode 100644
index 0000000..4437952
--- /dev/null
+++ b/DLS2/Enums/Level2ArticulatorEnums.cs
@@ -0,0 +1,69 @@
+namespace Kermalis.DLS2
+{
+ public enum Level2ArticulatorSource : ushort
+ {
+ None = 0x0,
+ LFO = 0x1,
+ KeyOnVelocity = 0x2,
+ KeyNumber = 0x3,
+ EG1 = 0x4,
+ EG2 = 0x5,
+ PitchWheel = 0x6,
+ PolyPressure = 0x7,
+ ChannelPressure = 0x8,
+ Vibrato = 0x9,
+ Modulation_CC1 = 0x81,
+ ChannelVolume_CC7 = 0x87,
+ Pan_CC10 = 0x8A,
+ Expression_CC11 = 0x8B,
+ ChorusSend_CC91 = 0xDB,
+ Reverb_SendCC93 = 0xDD,
+ PitchBendRange_RPN0 = 0x100,
+ FineTune_RPN1 = 0x101,
+ CoarseTune_RPN2 = 0x102
+ }
+
+ public enum Level2ArticulatorDestination : ushort
+ {
+ None = 0x0,
+ Gain = 0x1,
+ Pitch = 0x3,
+ Pan = 0x4,
+ KeyNumber = 0x5,
+ Left = 0x10,
+ Right = 0x11,
+ Center = 0x12,
+ LFEChannel = 0x13,
+ LeftRear = 0x14,
+ RightRear = 0x15,
+ Chorus = 0x80,
+ Reverb = 0x81,
+ LFOFrequency = 0x104,
+ LFOStartDelay = 0x105,
+ VIBFrequency = 0x114,
+ VIBStartDelay = 0x115,
+ EG1AttackTime = 0x206,
+ EG1DecayTime = 0x207,
+ EG1ReleaseTime = 0x209,
+ EG1SustainLevel = 0x20A,
+ EG1DelayTime = 0x20B,
+ EG1HoldTime = 0x20C,
+ EG1ShutdownTime = 0x20D,
+ EG2AttackTime = 0x30A,
+ EG2DecayTime = 0x30B,
+ EG2ReleaseTime = 0x30D,
+ EG2SustainLevel = 0x30E,
+ EG2DelayTime = 0x30F,
+ EG2HoldTime = 0x310,
+ FilterCutoff = 0x500,
+ FilterResonance = 0x501
+ }
+
+ public enum Level2ArticulatorTransform : byte
+ {
+ None = 0x0,
+ Concave = 0x1,
+ Convex = 0x2,
+ Switch = 0x3
+ }
+}
diff --git a/DLS2/Enums/WaveFormat.cs b/DLS2/Enums/WaveFormat.cs
new file mode 100644
index 0000000..4cab797
--- /dev/null
+++ b/DLS2/Enums/WaveFormat.cs
@@ -0,0 +1,15 @@
+namespace Kermalis.DLS2
+{
+ public enum WaveFormat : ushort
+ {
+ Unknown = 0,
+ PCM = 1,
+ MSADPCM = 2,
+ Float = 3,
+ ALaw = 6,
+ MuLaw = 7,
+ DVIADPCM = 17,
+ IMAADPCM = 17,
+ Extensible = 0xFFFE
+ }
+}
diff --git a/DLS2/Enums/WaveLinkChannels.cs b/DLS2/Enums/WaveLinkChannels.cs
new file mode 100644
index 0000000..678bd25
--- /dev/null
+++ b/DLS2/Enums/WaveLinkChannels.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Kermalis.DLS2
+{
+ [Flags]
+ public enum WaveLinkChannels : uint
+ {
+ None = 0,
+ Left = 1 << 0,
+ Right = 1 << 1,
+ Center = 1 << 2,
+ LowFrequencyEnergy = 1 << 3,
+ SurroundLeft = 1 << 4,
+ SurroundRight = 1 << 5,
+ LeftOfCenter = 1 << 6,
+ RightOfCenter = 1 << 7,
+ SurroundCenter = 1 << 8,
+ SideLeft = 1 << 9,
+ SideRight = 1 << 10,
+ Top = 1 << 11,
+ TopFrontLeft = 1 << 12,
+ TopFrontCenter = 1 << 13,
+ TopFrontRight = 1 << 14,
+ TopRearLeft = 1 << 15,
+ TopRearCenter = 1 << 16,
+ TopRearRight = 1 << 17
+ }
+}
diff --git a/DLS2/Enums/WaveLinkOptions.cs b/DLS2/Enums/WaveLinkOptions.cs
new file mode 100644
index 0000000..7f47d8c
--- /dev/null
+++ b/DLS2/Enums/WaveLinkOptions.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Kermalis.DLS2
+{
+ [Flags]
+ public enum WaveLinkOptions : ushort
+ {
+ None = 0,
+ PhaseMaster = 1 << 0,
+ MultiChannel = 1 << 1
+ }
+}
diff --git a/DLS2/Enums/WaveSampleLoop.cs b/DLS2/Enums/WaveSampleLoop.cs
new file mode 100644
index 0000000..68b6313
--- /dev/null
+++ b/DLS2/Enums/WaveSampleLoop.cs
@@ -0,0 +1,8 @@
+namespace Kermalis.DLS2
+{
+ public enum LoopType : uint
+ {
+ Forward = 0,
+ Release = 1
+ }
+}
diff --git a/DLS2/Enums/WaveSampleOptions.cs b/DLS2/Enums/WaveSampleOptions.cs
new file mode 100644
index 0000000..ef87bab
--- /dev/null
+++ b/DLS2/Enums/WaveSampleOptions.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Kermalis.DLS2
+{
+ [Flags]
+ public enum WaveSampleOptions : uint
+ {
+ None = 0,
+ NoTruncation = 1 << 0,
+ NoCompression = 1 << 1
+ }
+}
diff --git a/DLS2/Structs/ConnectionBlock.cs b/DLS2/Structs/ConnectionBlock.cs
new file mode 100644
index 0000000..d8a9f75
--- /dev/null
+++ b/DLS2/Structs/ConnectionBlock.cs
@@ -0,0 +1,163 @@
+using Kermalis.EndianBinaryIO;
+using System;
+
+namespace Kermalis.DLS2
+{
+ public sealed class Level1ArticulatorConnectionBlock
+ {
+ public Level1ArticulatorSource Source { get; set; }
+ public Level1ArticulatorSource Control { get; set; }
+ public Level1ArticulatorDestination Destination { get; set; }
+ public Level1ArticulatorTransform Transform { get; set; }
+ public int Scale { get; set; }
+
+ public Level1ArticulatorConnectionBlock() { }
+ internal Level1ArticulatorConnectionBlock(EndianBinaryReader reader)
+ {
+ Source = reader.ReadEnum();
+ Control = reader.ReadEnum();
+ Destination = reader.ReadEnum();
+ Transform = reader.ReadEnum();
+ Scale = reader.ReadInt32();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(Source);
+ writer.Write(Control);
+ writer.Write(Destination);
+ writer.Write(Transform);
+ writer.Write(Scale);
+ }
+ }
+
+ public sealed class Level2ArticulatorConnectionBlock
+ {
+ public Level2ArticulatorSource Source { get; set; }
+ public Level2ArticulatorSource Control { get; set; }
+ public Level2ArticulatorDestination Destination { get; set; }
+ public ushort Transform_Raw { get; set; }
+ public int Scale { get; set; }
+
+ public bool InvertSource
+ {
+ get => (Transform_Raw >> 15) != 0;
+ set
+ {
+ if (value)
+ {
+ Transform_Raw |= 1 << 15;
+ }
+ else
+ {
+ Transform_Raw &= unchecked((ushort)~(1 << 15));
+ }
+ }
+ }
+ public bool BipolarSource
+ {
+ get => ((Transform_Raw >> 14) & 1) != 0;
+ set
+ {
+ if (value)
+ {
+ Transform_Raw |= 1 << 14;
+ }
+ else
+ {
+ Transform_Raw &= unchecked((ushort)~(1 << 14));
+ }
+ }
+ }
+ public Level2ArticulatorTransform TransformSource
+ {
+ get => (Level2ArticulatorTransform)((Transform_Raw >> 10) & 0xF);
+ set
+ {
+ if (value > (Level2ArticulatorTransform)0xF)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Transform_Raw &= unchecked((ushort)~(0xF << 10));
+ Transform_Raw |= (ushort)((ushort)value << 10);
+ }
+ }
+
+ public bool InvertDestination
+ {
+ get => ((Transform_Raw >> 9) & 1) != 0;
+ set
+ {
+ if (value)
+ {
+ Transform_Raw |= 1 << 9;
+ }
+ else
+ {
+ Transform_Raw &= unchecked((ushort)~(1 << 9));
+ }
+ }
+ }
+ public bool BipolarDestination
+ {
+ get => ((Transform_Raw >> 8) & 1) != 0;
+ set
+ {
+ if (value)
+ {
+ Transform_Raw |= 1 << 8;
+ }
+ else
+ {
+ Transform_Raw &= unchecked((ushort)~(1 << 8));
+ }
+ }
+ }
+ public Level2ArticulatorTransform TransformDestination
+ {
+ get => (Level2ArticulatorTransform)((Transform_Raw >> 4) & 0xF);
+ set
+ {
+ if (value > (Level2ArticulatorTransform)0xF)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Transform_Raw &= unchecked((ushort)~(0xF << 4));
+ Transform_Raw |= (ushort)((ushort)value << 4);
+ }
+ }
+
+ public Level2ArticulatorTransform TransformOutput
+ {
+ get => (Level2ArticulatorTransform)(Transform_Raw & 0xF);
+ set
+ {
+ if (value > (Level2ArticulatorTransform)0xF)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Transform_Raw &= unchecked((ushort)~0xF);
+ Transform_Raw |= (ushort)value;
+ }
+ }
+
+ public Level2ArticulatorConnectionBlock() { }
+ internal Level2ArticulatorConnectionBlock(EndianBinaryReader reader)
+ {
+ Source = reader.ReadEnum();
+ Control = reader.ReadEnum();
+ Destination = reader.ReadEnum();
+ Transform_Raw = reader.ReadUInt16();
+ Scale = reader.ReadInt32();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(Source);
+ writer.Write(Control);
+ writer.Write(Destination);
+ writer.Write(Transform_Raw);
+ writer.Write(Scale);
+ }
+ }
+}
diff --git a/DLS2/Structs/DLSID.cs b/DLS2/Structs/DLSID.cs
new file mode 100644
index 0000000..d955ef7
--- /dev/null
+++ b/DLS2/Structs/DLSID.cs
@@ -0,0 +1,122 @@
+using Kermalis.EndianBinaryIO;
+using System;
+#if !DEBUG
+using System.Collections.Generic;
+#endif
+using System.Linq;
+
+namespace Kermalis.DLS2
+{
+ public sealed class DLSID
+ {
+ public uint Data1 { get; set; }
+ public ushort Data2 { get; set; }
+ public ushort Data3 { get; set; }
+ public byte[] Data4 { get; }
+
+ public static DLSID Query_GMInHardware { get; } = new DLSID(0x178F2F24, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 });
+ public static DLSID Query_GSInHardware { get; } = new DLSID(0x178F2F25, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 });
+ public static DLSID Query_XGInHardware { get; } = new DLSID(0x178F2F26, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 });
+ public static DLSID Query_SupportsDLS1 { get; } = new DLSID(0x178F2F27, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 });
+ public static DLSID Query_SampleMemorySize { get; } = new DLSID(0x178F2F28, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 });
+ public static DLSID Query_SamplePlaybackRate { get; } = new DLSID(0x2A91F713, 0xA4BF, 0x11D2, new byte[] { 0xBB, 0xDF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 });
+ public static DLSID Query_ManufacturersID { get; } = new DLSID(0xB03E1181, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 });
+ public static DLSID Query_ProductID { get; } = new DLSID(0xB03E1182, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 });
+ public static DLSID Query_SupportsDLS2 { get; } = new DLSID(0xF14599E5, 0x4689, 0x11D2, new byte[] { 0xAF, 0xA6, 0x00, 0xAA, 0x00, 0x24, 0xD8, 0xB6 });
+
+ public DLSID()
+ {
+ Data4 = new byte[8];
+ }
+ internal DLSID(EndianBinaryReader reader)
+ {
+ Data1 = reader.ReadUInt32();
+ Data2 = reader.ReadUInt16();
+ Data3 = reader.ReadUInt16();
+ Data4 = reader.ReadBytes(8);
+ }
+ public DLSID(uint data1, ushort data2, ushort data3, byte[] data4)
+ {
+ if (data4 is null)
+ {
+ throw new ArgumentNullException(nameof(data4));
+ }
+ if (data4.Length != 8)
+ {
+ throw new ArgumentOutOfRangeException(nameof(data4.Length));
+ }
+ Data1 = data1;
+ Data2 = data2;
+ Data3 = data3;
+ Data4 = data4;
+ }
+ public DLSID(byte[] data)
+ {
+ if (data is null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+ if (data.Length != 16)
+ {
+ throw new ArgumentOutOfRangeException(nameof(data.Length));
+ }
+ Data1 = (uint)EndianBitConverter.BytesToInt32(data, 0, Endianness.LittleEndian);
+ Data2 = (ushort)EndianBitConverter.BytesToInt16(data, 4, Endianness.LittleEndian);
+ Data3 = (ushort)EndianBitConverter.BytesToInt16(data, 6, Endianness.LittleEndian);
+ Data4 = new byte[8];
+ for (int i = 0; i < 8; i++)
+ {
+ Data4[i] = data[8 + i];
+ }
+ }
+
+ public void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(Data1);
+ writer.Write(Data2);
+ writer.Write(Data3);
+ writer.Write(Data4);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(obj, this))
+ {
+ return true;
+ }
+ if (obj is DLSID id)
+ {
+ return id.Data1 == Data1 && id.Data2 == Data2 && id.Data3 == Data3 && id.Data4.SequenceEqual(Data4);
+ }
+ return false;
+ }
+ public override int GetHashCode()
+ {
+ // .NET Standard does not have this method
+#if DEBUG
+ return HashCode.Combine(Data1, Data2, Data3, Data4);
+#else
+ int hashCode = -0x8CAC62A;
+ hashCode = hashCode * -0x5AAAAAD7 + Data1.GetHashCode();
+ hashCode = hashCode * -0x5AAAAAD7 + Data2.GetHashCode();
+ hashCode = hashCode * -0x5AAAAAD7 + Data3.GetHashCode();
+ hashCode = hashCode * -0x5AAAAAD7 + EqualityComparer.Default.GetHashCode(Data4);
+ return hashCode;
+#endif
+ }
+ public override string ToString()
+ {
+ string str = Data1.ToString("X8") + '-' + Data2.ToString("X4") + '-' + Data3.ToString("X4") + '-';
+ for (int i = 0; i < 2; i++)
+ {
+ str += Data4[i].ToString("X2");
+ }
+ str += '-';
+ for (int i = 2; i < 8; i++)
+ {
+ str += Data4[i].ToString("X2");
+ }
+ return str;
+ }
+ }
+}
diff --git a/DLS2/Structs/MIDILocale.cs b/DLS2/Structs/MIDILocale.cs
new file mode 100644
index 0000000..49cd325
--- /dev/null
+++ b/DLS2/Structs/MIDILocale.cs
@@ -0,0 +1,87 @@
+using Kermalis.EndianBinaryIO;
+using System;
+
+namespace Kermalis.DLS2
+{
+ // MIDILOCALE - Page 45 of spec
+ public sealed class MIDILocale
+ {
+ public uint Bank_Raw { get; set; }
+ public uint Instrument_Raw { get; set; }
+
+ public byte CC32
+ {
+ get => (byte)(Bank_Raw & 0x7F);
+ set
+ {
+ if (value > 0x7F)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Bank_Raw &= unchecked((uint)~0x7F);
+ Bank_Raw |= value;
+ }
+ }
+ public byte CC0
+ {
+ get => (byte)((Bank_Raw >> 7) & 0x7F);
+ set
+ {
+ if (value > 0x7F)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Bank_Raw &= unchecked((uint)~(0x7F << 7));
+ Bank_Raw |= (uint)(value << 7);
+ }
+ }
+ public bool IsDrum
+ {
+ get => (Bank_Raw >> 31) != 0;
+ set
+ {
+ if (value)
+ {
+ Bank_Raw |= 1u << 31;
+ }
+ else
+ {
+ Bank_Raw &= ~(1 << 31);
+ }
+ }
+ }
+ public byte Instrument
+ {
+ get => (byte)(Instrument_Raw & 0x7F);
+ set
+ {
+ if (value > 0x7F)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ Instrument_Raw &= unchecked((uint)~0x7F);
+ Instrument_Raw |= value;
+ }
+ }
+
+ public MIDILocale() { }
+ public MIDILocale(byte cc32, byte cc0, bool isDrum, byte instrument)
+ {
+ CC32 = cc32;
+ CC0 = cc0;
+ IsDrum = isDrum;
+ Instrument = instrument;
+ }
+ internal MIDILocale(EndianBinaryReader reader)
+ {
+ Bank_Raw = reader.ReadUInt32();
+ Instrument_Raw = reader.ReadUInt32();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(Bank_Raw);
+ writer.Write(Instrument_Raw);
+ }
+ }
+}
diff --git a/DLS2/Structs/Range.cs b/DLS2/Structs/Range.cs
new file mode 100644
index 0000000..7248bf8
--- /dev/null
+++ b/DLS2/Structs/Range.cs
@@ -0,0 +1,28 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class Range
+ {
+ public ushort Low { get; set; }
+ public ushort High { get; set; }
+
+ public Range() { }
+ public Range(ushort low, ushort high)
+ {
+ Low = low;
+ High = high;
+ }
+ internal Range(EndianBinaryReader reader)
+ {
+ Low = reader.ReadUInt16();
+ High = reader.ReadUInt16();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(Low);
+ writer.Write(High);
+ }
+ }
+}
diff --git a/DLS2/Structs/WaveInfo.cs b/DLS2/Structs/WaveInfo.cs
new file mode 100644
index 0000000..2f1cb25
--- /dev/null
+++ b/DLS2/Structs/WaveInfo.cs
@@ -0,0 +1,32 @@
+using Kermalis.EndianBinaryIO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class WaveInfo
+ {
+ public WaveFormat FormatTag { get; set; }
+ public ushort Channels { get; set; }
+ public uint SamplesPerSec { get; set; }
+ public uint AvgBytesPerSec { get; set; }
+ public ushort BlockAlign { get; set; }
+
+ internal WaveInfo() { }
+ internal WaveInfo(EndianBinaryReader reader)
+ {
+ FormatTag = reader.ReadEnum();
+ Channels = reader.ReadUInt16();
+ SamplesPerSec = reader.ReadUInt32();
+ AvgBytesPerSec = reader.ReadUInt32();
+ BlockAlign = reader.ReadUInt16();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(FormatTag);
+ writer.Write(Channels);
+ writer.Write(SamplesPerSec);
+ writer.Write(AvgBytesPerSec);
+ writer.Write(BlockAlign);
+ }
+ }
+}
diff --git a/DLS2/Structs/WaveSampleLoop.cs b/DLS2/Structs/WaveSampleLoop.cs
new file mode 100644
index 0000000..8e98a54
--- /dev/null
+++ b/DLS2/Structs/WaveSampleLoop.cs
@@ -0,0 +1,33 @@
+using Kermalis.EndianBinaryIO;
+using System.IO;
+
+namespace Kermalis.DLS2
+{
+ public sealed class WaveSampleLoop
+ {
+ public LoopType LoopType { get; set; }
+ public uint LoopStart { get; set; }
+ public uint LoopLength { get; set; }
+
+ public WaveSampleLoop() { }
+ internal WaveSampleLoop(EndianBinaryReader reader)
+ {
+ uint byteSize = reader.ReadUInt32();
+ if (byteSize != 16)
+ {
+ throw new InvalidDataException();
+ }
+ LoopType = reader.ReadEnum();
+ LoopStart = reader.ReadUInt32();
+ LoopLength = reader.ReadUInt32();
+ }
+
+ internal void Write(EndianBinaryWriter writer)
+ {
+ writer.Write(16u);
+ writer.Write(LoopType);
+ writer.Write(LoopStart);
+ writer.Write(LoopLength);
+ }
+ }
+}
diff --git a/EndianBinaryIO/Attributes.cs b/EndianBinaryIO/Attributes.cs
new file mode 100644
index 0000000..8e3e98f
--- /dev/null
+++ b/EndianBinaryIO/Attributes.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Text;
+
+namespace Kermalis.EndianBinaryIO
+{
+ public interface IBinaryAttribute
+ {
+ T Value { get; }
+ }
+
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryIgnoreAttribute : Attribute, IBinaryAttribute
+ {
+ public bool Value { get; }
+
+ public BinaryIgnoreAttribute(bool ignore = true)
+ {
+ Value = ignore;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryBooleanSizeAttribute : Attribute, IBinaryAttribute
+ {
+ public BooleanSize Value { get; }
+
+ public BinaryBooleanSizeAttribute(BooleanSize booleanSize)
+ {
+ if (booleanSize >= BooleanSize.MAX)
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(BinaryBooleanSizeAttribute)} cannot be created with a size of {booleanSize}.");
+ }
+ Value = booleanSize;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryEncodingAttribute : Attribute, IBinaryAttribute
+ {
+ public Encoding Value { get; }
+
+ public BinaryEncodingAttribute(string encodingName)
+ {
+ Value = Encoding.GetEncoding(encodingName);
+ }
+ public BinaryEncodingAttribute(int encodingCodepage)
+ {
+ Value = Encoding.GetEncoding(encodingCodepage);
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryStringNullTerminatedAttribute : Attribute, IBinaryAttribute
+ {
+ public bool Value { get; }
+
+ public BinaryStringNullTerminatedAttribute(bool nullTerminated = true)
+ {
+ Value = nullTerminated;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryArrayFixedLengthAttribute : Attribute, IBinaryAttribute
+ {
+ public int Value { get; }
+
+ public BinaryArrayFixedLengthAttribute(int length)
+ {
+ if (length < 0)
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(BinaryArrayFixedLengthAttribute)} cannot be created with a length of {length}. Length must be 0 or greater.");
+ }
+ Value = length;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryArrayVariableLengthAttribute : Attribute, IBinaryAttribute
+ {
+ public string Value { get; }
+
+ public BinaryArrayVariableLengthAttribute(string anchor)
+ {
+ Value = anchor;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryStringFixedLengthAttribute : Attribute, IBinaryAttribute
+ {
+ public int Value { get; }
+
+ public BinaryStringFixedLengthAttribute(int length)
+ {
+ if (length <= 0)
+ {
+ throw new ArgumentOutOfRangeException($"{nameof(BinaryStringFixedLengthAttribute)} cannot be created with a length of {length}. Length must be 0 or greater.");
+ }
+ Value = length;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryStringVariableLengthAttribute : Attribute, IBinaryAttribute
+ {
+ public string Value { get; }
+
+ public BinaryStringVariableLengthAttribute(string anchor)
+ {
+ Value = anchor;
+ }
+ }
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class BinaryStringTrimNullTerminatorsAttribute : Attribute, IBinaryAttribute
+ {
+ public bool Value { get; }
+
+ public BinaryStringTrimNullTerminatorsAttribute(bool trim = true)
+ {
+ Value = trim;
+ }
+ }
+}
diff --git a/EndianBinaryIO/EndianBinaryIO.csproj b/EndianBinaryIO/EndianBinaryIO.csproj
new file mode 100644
index 0000000..2aa1ec8
--- /dev/null
+++ b/EndianBinaryIO/EndianBinaryIO.csproj
@@ -0,0 +1,39 @@
+
+
+
+ netcoreapp3.1
+ Library
+ Kermalis.EndianBinaryIO
+ Kermalis
+ Kermalis
+ EndianBinaryIO
+ EndianBinaryIO
+ EndianBinaryIO
+ 1.1.2.0
+ https://github.com/Kermalis/EndianBinaryIO
+ git
+
+ true
+
+ A .NET Core library compatible with any newer .NET SDK versions. This library can read and write primitives, enums, arrays, and strings to streams and byte arrays using specified endianness, string encoding, and boolean sizes.
+Objects can also be read from/written to streams via reflection and attributes.
+Project URL ― https://github.com/Kermalis/EndianBinaryIO
+ https://github.com/Kermalis/EndianBinaryIO
+ en-001
+ Serialization;Reflection;Endianness;LittleEndian;BigEndian;EndianBinaryIO
+
+
+
+ Auto
+ none
+ false
+
+
+
+ false
+ DEBUG;TRACE
+ full
+ true
+
+
+
diff --git a/EndianBinaryIO/EndianBinaryReader.cs b/EndianBinaryIO/EndianBinaryReader.cs
new file mode 100644
index 0000000..de2ec62
--- /dev/null
+++ b/EndianBinaryIO/EndianBinaryReader.cs
@@ -0,0 +1,893 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace Kermalis.EndianBinaryIO
+{
+ public class EndianBinaryReader
+ {
+ public Stream BaseStream { get; }
+ private Endianness _endianness;
+ public Endianness Endianness
+ {
+ get => _endianness;
+ set
+ {
+ if (value >= Endianness.MAX)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _endianness = value;
+ }
+ }
+ private BooleanSize _booleanSize;
+ public BooleanSize BooleanSize
+ {
+ get => _booleanSize;
+ set
+ {
+ if (value >= BooleanSize.MAX)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _booleanSize = value;
+ }
+ }
+ public Encoding Encoding { get; set; }
+
+ private byte[] _buffer;
+
+ public EndianBinaryReader(Stream baseStream, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8)
+ {
+ if (baseStream is null)
+ {
+ throw new ArgumentNullException(nameof(baseStream));
+ }
+ if (!baseStream.CanRead)
+ {
+ throw new ArgumentException(nameof(baseStream));
+ }
+ BaseStream = baseStream;
+ Endianness = endianness;
+ BooleanSize = booleanSize;
+ Encoding = Encoding.ASCII;
+ }
+ public EndianBinaryReader(Stream baseStream, Encoding encoding, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8)
+ {
+ if (baseStream is null)
+ {
+ throw new ArgumentNullException(nameof(baseStream));
+ }
+ if (!baseStream.CanRead)
+ {
+ throw new ArgumentException(nameof(baseStream));
+ }
+ BaseStream = baseStream;
+ Endianness = endianness;
+ BooleanSize = booleanSize;
+ Encoding = encoding;
+ }
+
+ private void ReadBytesIntoBuffer(int byteCount)
+ {
+ if (_buffer is null || _buffer.Length < byteCount)
+ {
+ _buffer = new byte[byteCount];
+ }
+ if (BaseStream.Read(_buffer, 0, byteCount) != byteCount)
+ {
+ throw new EndOfStreamException();
+ }
+ }
+ private char[] DecodeChars(Encoding encoding, int charCount)
+ {
+ Utils.ThrowIfCannotUseEncoding(encoding);
+ int maxBytes = encoding.GetMaxByteCount(charCount);
+ byte[] buffer = new byte[maxBytes];
+ int amtRead = BaseStream.Read(buffer, 0, maxBytes); // Do not throw EndOfStreamException if there aren't enough bytes at the end of the stream
+ if (amtRead == 0)
+ {
+ throw new EndOfStreamException();
+ }
+ // If the maxBytes would be 4, and the string only takes 2, we'd not have enough bytes, but if it's a proper string it doesn't matter
+ char[] chars = encoding.GetChars(buffer);
+ if (chars.Length < charCount)
+ {
+ throw new InvalidDataException(); // Too few chars means the decoding went wrong
+ }
+ // If we read too many chars, we need to shrink the array
+ // For example, if we want 1 char and the max bytes is 2, but we manage to read 2 1-byte chars, we'd want to shrink back to 1 char
+ Array.Resize(ref chars, charCount);
+ int actualBytes = encoding.GetByteCount(chars);
+ if (amtRead != actualBytes)
+ {
+ BaseStream.Position -= amtRead - actualBytes; // Set the stream back to compensate for the extra bytes we read
+ }
+ return chars;
+ }
+
+ public byte PeekByte()
+ {
+ long pos = BaseStream.Position;
+ byte b = ReadByte();
+ BaseStream.Position = pos;
+ return b;
+ }
+ public byte PeekByte(long offset)
+ {
+ BaseStream.Position = offset;
+ return PeekByte();
+ }
+ public byte[] PeekBytes(int count)
+ {
+ long pos = BaseStream.Position;
+ byte[] b = ReadBytes(count);
+ BaseStream.Position = pos;
+ return b;
+ }
+ public byte[] PeekBytes(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return PeekBytes(count);
+ }
+ public char PeekChar()
+ {
+ long pos = BaseStream.Position;
+ char c = ReadChar();
+ BaseStream.Position = pos;
+ return c;
+ }
+ public char PeekChar(long offset)
+ {
+ BaseStream.Position = offset;
+ return PeekChar();
+ }
+ public char PeekChar(Encoding encoding)
+ {
+ long pos = BaseStream.Position;
+ char c = ReadChar(encoding);
+ BaseStream.Position = pos;
+ return c;
+ }
+ public char PeekChar(Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return PeekChar(encoding);
+ }
+
+ public bool ReadBoolean()
+ {
+ return ReadBoolean(BooleanSize);
+ }
+ public bool ReadBoolean(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadBoolean(BooleanSize);
+ }
+ public bool ReadBoolean(BooleanSize booleanSize)
+ {
+ switch (booleanSize)
+ {
+ case BooleanSize.U8:
+ {
+ ReadBytesIntoBuffer(1);
+ return _buffer[0] != 0;
+ }
+ case BooleanSize.U16:
+ {
+ ReadBytesIntoBuffer(2);
+ return EndianBitConverter.BytesToInt16(_buffer, 0, Endianness) != 0;
+ }
+ case BooleanSize.U32:
+ {
+ ReadBytesIntoBuffer(4);
+ return EndianBitConverter.BytesToInt32(_buffer, 0, Endianness) != 0;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(booleanSize));
+ }
+ }
+ public bool ReadBoolean(BooleanSize booleanSize, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadBoolean(booleanSize);
+ }
+ public bool[] ReadBooleans(int count)
+ {
+ return ReadBooleans(count, BooleanSize);
+ }
+ public bool[] ReadBooleans(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadBooleans(count, BooleanSize);
+ }
+ public bool[] ReadBooleans(int count, BooleanSize size)
+ {
+ if (!Utils.ValidateReadArraySize(count, out bool[] array))
+ {
+ array = new bool[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ReadBoolean(size);
+ }
+ }
+ return array;
+ }
+ public bool[] ReadBooleans(int count, BooleanSize size, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadBooleans(count, size);
+ }
+ public byte ReadByte()
+ {
+ ReadBytesIntoBuffer(1);
+ return _buffer[0];
+ }
+ public byte ReadByte(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadByte();
+ }
+ public byte[] ReadBytes(int count)
+ {
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ ReadBytesIntoBuffer(count);
+ array = new byte[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = _buffer[i];
+ }
+ }
+ return array;
+ }
+ public byte[] ReadBytes(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadBytes(count);
+ }
+ public sbyte ReadSByte()
+ {
+ ReadBytesIntoBuffer(1);
+ return (sbyte)_buffer[0];
+ }
+ public sbyte ReadSByte(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadSByte();
+ }
+ public sbyte[] ReadSBytes(int count)
+ {
+ if (!Utils.ValidateReadArraySize(count, out sbyte[] array))
+ {
+ ReadBytesIntoBuffer(count);
+ array = new sbyte[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = (sbyte)_buffer[i];
+ }
+ }
+ return array;
+ }
+ public sbyte[] ReadSBytes(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadSBytes(count);
+ }
+ public char ReadChar()
+ {
+ return ReadChar(Encoding);
+ }
+ public char ReadChar(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadChar();
+ }
+ public char ReadChar(Encoding encoding)
+ {
+ return DecodeChars(encoding, 1)[0];
+ }
+ public char ReadChar(Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadChar(encoding);
+ }
+ public char[] ReadChars(int count, bool trimNullTerminators)
+ {
+ return ReadChars(count, trimNullTerminators, Encoding);
+ }
+ public char[] ReadChars(int count, bool trimNullTerminators, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadChars(count, trimNullTerminators);
+ }
+ public char[] ReadChars(int count, bool trimNullTerminators, Encoding encoding)
+ {
+ if (Utils.ValidateReadArraySize(count, out char[] array))
+ {
+ return array;
+ }
+ array = DecodeChars(encoding, count);
+ if (trimNullTerminators)
+ {
+ int i = Array.IndexOf(array, '\0');
+ if (i != -1)
+ {
+ Array.Resize(ref array, i);
+ }
+ }
+ return array;
+ }
+ public char[] ReadChars(int count, bool trimNullTerminators, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadChars(count, trimNullTerminators, encoding);
+ }
+ public string ReadStringNullTerminated()
+ {
+ return ReadStringNullTerminated(Encoding);
+ }
+ public string ReadStringNullTerminated(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStringNullTerminated();
+ }
+ public string ReadStringNullTerminated(Encoding encoding)
+ {
+ string text = string.Empty;
+ while (true)
+ {
+ char c = ReadChar(encoding);
+ if (c == '\0')
+ {
+ break;
+ }
+ text += c;
+ }
+ return text;
+ }
+ public string ReadStringNullTerminated(Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStringNullTerminated(encoding);
+ }
+ public string ReadString(int charCount, bool trimNullTerminators)
+ {
+ return ReadString(charCount, trimNullTerminators, Encoding);
+ }
+ public string ReadString(int charCount, bool trimNullTerminators, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadString(charCount, trimNullTerminators);
+ }
+ public string ReadString(int charCount, bool trimNullTerminators, Encoding encoding)
+ {
+ return new string(ReadChars(charCount, trimNullTerminators, encoding));
+ }
+ public string ReadString(int charCount, bool trimNullTerminators, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadString(charCount, trimNullTerminators, encoding);
+ }
+ public string[] ReadStringsNullTerminated(int count)
+ {
+ return ReadStringsNullTerminated(count, Encoding);
+ }
+ public string[] ReadStringsNullTerminated(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStringsNullTerminated(count);
+ }
+ public string[] ReadStringsNullTerminated(int count, Encoding encoding)
+ {
+ if (!Utils.ValidateReadArraySize(count, out string[] array))
+ {
+ array = new string[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ReadStringNullTerminated(encoding);
+ }
+ }
+ return array;
+ }
+ public string[] ReadStringsNullTerminated(int count, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStringsNullTerminated(count, encoding);
+ }
+ public string[] ReadStrings(int count, int charCount, bool trimNullTerminators)
+ {
+ return ReadStrings(count, charCount, trimNullTerminators, Encoding);
+ }
+ public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStrings(count, charCount, trimNullTerminators);
+ }
+ public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, Encoding encoding)
+ {
+ if (!Utils.ValidateReadArraySize(count, out string[] array))
+ {
+ array = new string[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ReadString(charCount, trimNullTerminators, encoding);
+ }
+ }
+ return array;
+ }
+ public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadStrings(count, charCount, trimNullTerminators, encoding);
+ }
+ public short ReadInt16()
+ {
+ ReadBytesIntoBuffer(2);
+ return EndianBitConverter.BytesToInt16(_buffer, 0, Endianness);
+ }
+ public short ReadInt16(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt16();
+ }
+ public short[] ReadInt16s(int count)
+ {
+ ReadBytesIntoBuffer(count * 2);
+ return EndianBitConverter.BytesToInt16s(_buffer, 0, count, Endianness);
+ }
+ public short[] ReadInt16s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt16s(count);
+ }
+ public ushort ReadUInt16()
+ {
+ ReadBytesIntoBuffer(2);
+ return (ushort)EndianBitConverter.BytesToInt16(_buffer, 0, Endianness);
+ }
+ public ushort ReadUInt16(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt16();
+ }
+ public ushort[] ReadUInt16s(int count)
+ {
+ ReadBytesIntoBuffer(count * 2);
+ return EndianBitConverter.BytesToUInt16s(_buffer, 0, count, Endianness);
+ }
+ public ushort[] ReadUInt16s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt16s(count);
+ }
+ public int ReadInt32()
+ {
+ ReadBytesIntoBuffer(4);
+ return EndianBitConverter.BytesToInt32(_buffer, 0, Endianness);
+ }
+ public int ReadInt32(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt32();
+ }
+ public int[] ReadInt32s(int count)
+ {
+ ReadBytesIntoBuffer(count * 4);
+ return EndianBitConverter.BytesToInt32s(_buffer, 0, count, Endianness);
+ }
+ public int[] ReadInt32s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt32s(count);
+ }
+ public uint ReadUInt32()
+ {
+ ReadBytesIntoBuffer(4);
+ return (uint)EndianBitConverter.BytesToInt32(_buffer, 0, Endianness);
+ }
+ public uint ReadUInt32(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt32();
+ }
+ public uint[] ReadUInt32s(int count)
+ {
+ ReadBytesIntoBuffer(count * 4);
+ return EndianBitConverter.BytesToUInt32s(_buffer, 0, count, Endianness);
+ }
+ public uint[] ReadUInt32s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt32s(count);
+ }
+ public long ReadInt64()
+ {
+ ReadBytesIntoBuffer(8);
+ return EndianBitConverter.BytesToInt64(_buffer, 0, Endianness);
+ }
+ public long ReadInt64(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt64();
+ }
+ public long[] ReadInt64s(int count)
+ {
+ ReadBytesIntoBuffer(count * 8);
+ return EndianBitConverter.BytesToInt64s(_buffer, 0, count, Endianness);
+ }
+ public long[] ReadInt64s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadInt64s(count);
+ }
+ public ulong ReadUInt64()
+ {
+ ReadBytesIntoBuffer(8);
+ return (ulong)EndianBitConverter.BytesToInt64(_buffer, 0, Endianness);
+ }
+ public ulong ReadUInt64(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt64();
+ }
+ public ulong[] ReadUInt64s(int count)
+ {
+ ReadBytesIntoBuffer(count * 8);
+ return EndianBitConverter.BytesToUInt64s(_buffer, 0, count, Endianness);
+ }
+ public ulong[] ReadUInt64s(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadUInt64s(count);
+ }
+ public float ReadSingle()
+ {
+ ReadBytesIntoBuffer(4);
+ return EndianBitConverter.BytesToSingle(_buffer, 0, Endianness);
+ }
+ public float ReadSingle(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadSingle();
+ }
+ public float[] ReadSingles(int count)
+ {
+ ReadBytesIntoBuffer(count * 4);
+ return EndianBitConverter.BytesToSingles(_buffer, 0, count, Endianness);
+ }
+ public float[] ReadSingles(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadSingles(count);
+ }
+ public double ReadDouble()
+ {
+ ReadBytesIntoBuffer(8);
+ return EndianBitConverter.BytesToDouble(_buffer, 0, Endianness);
+ }
+ public double ReadDouble(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDouble();
+ }
+ public double[] ReadDoubles(int count)
+ {
+ ReadBytesIntoBuffer(count * 8);
+ return EndianBitConverter.BytesToDoubles(_buffer, 0, count, Endianness);
+ }
+ public double[] ReadDoubles(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDoubles(count);
+ }
+ public decimal ReadDecimal()
+ {
+ ReadBytesIntoBuffer(16);
+ return EndianBitConverter.BytesToDecimal(_buffer, 0, Endianness);
+ }
+ public decimal ReadDecimal(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDecimal();
+ }
+ public decimal[] ReadDecimals(int count)
+ {
+ ReadBytesIntoBuffer(count * 16);
+ return EndianBitConverter.BytesToDecimals(_buffer, 0, count, Endianness);
+ }
+ public decimal[] ReadDecimals(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDecimals(count);
+ }
+
+ // Do not allow writing abstract "Enum" because there is no way to know which underlying type to read
+ // Yes "struct" restriction on reads
+ public TEnum ReadEnum() where TEnum : struct, Enum
+ {
+ Type enumType = typeof(TEnum);
+ Type underlyingType = Enum.GetUnderlyingType(enumType);
+ object value;
+ switch (Type.GetTypeCode(underlyingType))
+ {
+ case TypeCode.Byte: value = ReadByte(); break;
+ case TypeCode.SByte: value = ReadSByte(); break;
+ case TypeCode.Int16: value = ReadInt16(); break;
+ case TypeCode.UInt16: value = ReadUInt16(); break;
+ case TypeCode.Int32: value = ReadInt32(); break;
+ case TypeCode.UInt32: value = ReadUInt32(); break;
+ case TypeCode.Int64: value = ReadInt64(); break;
+ case TypeCode.UInt64: value = ReadUInt64(); break;
+ default: throw new ArgumentOutOfRangeException(nameof(underlyingType));
+ }
+ return (TEnum)Enum.ToObject(enumType, value);
+ }
+ public TEnum ReadEnum(long offset) where TEnum : struct, Enum
+ {
+ BaseStream.Position = offset;
+ return ReadEnum();
+ }
+ public TEnum[] ReadEnums(int count) where TEnum : struct, Enum
+ {
+ if (!Utils.ValidateReadArraySize(count, out TEnum[] array))
+ {
+ array = new TEnum[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ReadEnum();
+ }
+ }
+ return array;
+ }
+ public TEnum[] ReadEnums(int count, long offset) where TEnum : struct, Enum
+ {
+ BaseStream.Position = offset;
+ return ReadEnums(count);
+ }
+
+ public DateTime ReadDateTime()
+ {
+ return DateTime.FromBinary(ReadInt64());
+ }
+ public DateTime ReadDateTime(long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDateTime();
+ }
+ public DateTime[] ReadDateTimes(int count)
+ {
+ if (!Utils.ValidateReadArraySize(count, out DateTime[] array))
+ {
+ array = new DateTime[count];
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ReadDateTime();
+ }
+ }
+ return array;
+ }
+ public DateTime[] ReadDateTimes(int count, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadDateTimes(count);
+ }
+
+ public T ReadObject() where T : new()
+ {
+ return (T)ReadObject(typeof(T));
+ }
+ public object ReadObject(Type objType)
+ {
+ Utils.ThrowIfCannotReadWriteType(objType);
+ object obj = Activator.CreateInstance(objType);
+ ReadIntoObject(obj);
+ return obj;
+ }
+ public T ReadObject(long offset) where T : new()
+ {
+ BaseStream.Position = offset;
+ return ReadObject();
+ }
+ public object ReadObject(Type objType, long offset)
+ {
+ BaseStream.Position = offset;
+ return ReadObject(objType);
+ }
+ public void ReadIntoObject(IBinarySerializable obj)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ obj.Read(this);
+ }
+ public void ReadIntoObject(IBinarySerializable obj, long offset)
+ {
+ BaseStream.Position = offset;
+ ReadIntoObject(obj);
+ }
+ public void ReadIntoObject(object obj)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ if (obj is IBinarySerializable bs)
+ {
+ bs.Read(this);
+ return;
+ }
+
+ Type objType = obj.GetType();
+ Utils.ThrowIfCannotReadWriteType(objType);
+
+ // Get public non-static properties
+ foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
+ {
+ if (Utils.AttributeValueOrDefault(propertyInfo, false))
+ {
+ continue; // Skip properties with BinaryIgnoreAttribute
+ }
+
+ Type propertyType = propertyInfo.PropertyType;
+ object value;
+
+ if (propertyType.IsArray)
+ {
+ int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo);
+ // Get array type
+ Type elementType = propertyType.GetElementType();
+ if (arrayLength == 0)
+ {
+ value = Array.CreateInstance(elementType, 0); // Create 0 length array regardless of type
+ }
+ else
+ {
+ if (elementType.IsEnum)
+ {
+ elementType = Enum.GetUnderlyingType(elementType);
+ }
+ switch (Type.GetTypeCode(elementType))
+ {
+ case TypeCode.Boolean:
+ {
+ BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize);
+ value = ReadBooleans(arrayLength, booleanSize);
+ break;
+ }
+ case TypeCode.Byte: value = ReadBytes(arrayLength); break;
+ case TypeCode.SByte: value = ReadSBytes(arrayLength); break;
+ case TypeCode.Char:
+ {
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false);
+ value = ReadChars(arrayLength, trimNullTerminators, encoding);
+ break;
+ }
+ case TypeCode.Int16: value = ReadInt16s(arrayLength); break;
+ case TypeCode.UInt16: value = ReadUInt16s(arrayLength); break;
+ case TypeCode.Int32: value = ReadInt32s(arrayLength); break;
+ case TypeCode.UInt32: value = ReadUInt32s(arrayLength); break;
+ case TypeCode.Int64: value = ReadInt64s(arrayLength); break;
+ case TypeCode.UInt64: value = ReadUInt64s(arrayLength); break;
+ case TypeCode.Single: value = ReadSingles(arrayLength); break;
+ case TypeCode.Double: value = ReadDoubles(arrayLength); break;
+ case TypeCode.Decimal: value = ReadDecimals(arrayLength); break;
+ case TypeCode.DateTime: value = ReadDateTimes(arrayLength); break;
+ case TypeCode.String:
+ {
+ Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength);
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ if (nullTerminated == true)
+ {
+ value = ReadStringsNullTerminated(arrayLength, encoding);
+ }
+ else
+ {
+ bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false);
+ value = ReadStrings(arrayLength, stringLength, trimNullTerminators, encoding);
+ }
+ break;
+ }
+ case TypeCode.Object:
+ {
+ value = Array.CreateInstance(elementType, arrayLength);
+ if (typeof(IBinarySerializable).IsAssignableFrom(elementType))
+ {
+ for (int i = 0; i < arrayLength; i++)
+ {
+ var serializable = (IBinarySerializable)Activator.CreateInstance(elementType);
+ serializable.Read(this);
+ ((Array)value).SetValue(serializable, i);
+ }
+ }
+ else // Element's type is not supported so try to read the array's objects
+ {
+ for (int i = 0; i < arrayLength; i++)
+ {
+ object elementObj = ReadObject(elementType);
+ ((Array)value).SetValue(elementObj, i);
+ }
+ }
+ break;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(elementType));
+ }
+ }
+ }
+ else
+ {
+ if (propertyType.IsEnum)
+ {
+ propertyType = Enum.GetUnderlyingType(propertyType);
+ }
+ switch (Type.GetTypeCode(propertyType))
+ {
+ case TypeCode.Boolean:
+ {
+ BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize);
+ value = ReadBoolean(booleanSize);
+ break;
+ }
+ case TypeCode.Byte: value = ReadByte(); break;
+ case TypeCode.SByte: value = ReadSByte(); break;
+ case TypeCode.Char:
+ {
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ value = ReadChar(encoding);
+ break;
+ }
+ case TypeCode.Int16: value = ReadInt16(); break;
+ case TypeCode.UInt16: value = ReadUInt16(); break;
+ case TypeCode.Int32: value = ReadInt32(); break;
+ case TypeCode.UInt32: value = ReadUInt32(); break;
+ case TypeCode.Int64: value = ReadInt64(); break;
+ case TypeCode.UInt64: value = ReadUInt64(); break;
+ case TypeCode.Single: value = ReadSingle(); break;
+ case TypeCode.Double: value = ReadDouble(); break;
+ case TypeCode.Decimal: value = ReadDecimal(); break;
+ case TypeCode.DateTime: value = ReadDateTime(); break;
+ case TypeCode.String:
+ {
+ Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength);
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ if (nullTerminated == true)
+ {
+ value = ReadStringNullTerminated(encoding);
+ }
+ else
+ {
+ bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false);
+ value = ReadString(stringLength, trimNullTerminators, encoding);
+ }
+ break;
+ }
+ case TypeCode.Object:
+ {
+ if (typeof(IBinarySerializable).IsAssignableFrom(propertyType))
+ {
+ value = Activator.CreateInstance(propertyType);
+ ((IBinarySerializable)value).Read(this);
+ }
+ else // The property's type is not supported so try to read the object
+ {
+ value = ReadObject(propertyType);
+ }
+ break;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(propertyType));
+ }
+ }
+
+ // Set the value into the property
+ propertyInfo.SetValue(obj, value);
+ }
+ }
+ public void ReadIntoObject(object obj, long offset)
+ {
+ BaseStream.Position = offset;
+ ReadIntoObject(obj);
+ }
+ }
+}
diff --git a/EndianBinaryIO/EndianBinaryWriter.cs b/EndianBinaryIO/EndianBinaryWriter.cs
new file mode 100644
index 0000000..7800f28
--- /dev/null
+++ b/EndianBinaryIO/EndianBinaryWriter.cs
@@ -0,0 +1,926 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace Kermalis.EndianBinaryIO
+{
+ public class EndianBinaryWriter
+ {
+ public Stream BaseStream { get; }
+ private Endianness _endianness;
+ public Endianness Endianness
+ {
+ get => _endianness;
+ set
+ {
+ if (value >= Endianness.MAX)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _endianness = value;
+ }
+ }
+ private BooleanSize _booleanSize;
+ public BooleanSize BooleanSize
+ {
+ get => _booleanSize;
+ set
+ {
+ if (value >= BooleanSize.MAX)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ _booleanSize = value;
+ }
+ }
+ public Encoding Encoding { get; set; }
+
+ private byte[] _buffer;
+
+ public EndianBinaryWriter(Stream baseStream, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8)
+ {
+ if (baseStream is null)
+ {
+ throw new ArgumentNullException(nameof(baseStream));
+ }
+ if (!baseStream.CanWrite)
+ {
+ throw new ArgumentException(nameof(baseStream));
+ }
+ BaseStream = baseStream;
+ Endianness = endianness;
+ BooleanSize = booleanSize;
+ Encoding = Encoding.Default;
+ }
+ public EndianBinaryWriter(Stream baseStream, Encoding encoding, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8)
+ {
+ if (baseStream is null)
+ {
+ throw new ArgumentNullException(nameof(baseStream));
+ }
+ if (!baseStream.CanWrite)
+ {
+ throw new ArgumentException(nameof(baseStream));
+ }
+ BaseStream = baseStream;
+ Endianness = endianness;
+ BooleanSize = booleanSize;
+ Encoding = encoding;
+ }
+
+ private void SetBufferSize(int size)
+ {
+ if (_buffer is null || _buffer.Length < size)
+ {
+ _buffer = new byte[size];
+ }
+ }
+ private void WriteBytesFromBuffer(int byteCount)
+ {
+ BaseStream.Write(_buffer, 0, byteCount);
+ }
+
+ public void Write(bool value)
+ {
+ Write(value, BooleanSize);
+ }
+ public void Write(bool value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, BooleanSize);
+ }
+ public void Write(bool value, BooleanSize booleanSize)
+ {
+ switch (booleanSize)
+ {
+ case BooleanSize.U8:
+ {
+ SetBufferSize(1);
+ _buffer[0] = value ? (byte)1 : (byte)0;
+ WriteBytesFromBuffer(1);
+ break;
+ }
+ case BooleanSize.U16:
+ {
+ _buffer = EndianBitConverter.Int16ToBytes(value ? (short)1 : (short)0, Endianness);
+ WriteBytesFromBuffer(2);
+ break;
+ }
+ case BooleanSize.U32:
+ {
+ _buffer = EndianBitConverter.Int32ToBytes(value ? 1 : 0, Endianness);
+ WriteBytesFromBuffer(4);
+ break;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(booleanSize));
+ }
+ }
+ public void Write(bool value, BooleanSize booleanSize, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, booleanSize);
+ }
+ public void Write(bool[] value)
+ {
+ Write(value, 0, value.Length, BooleanSize);
+ }
+ public void Write(bool[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length, BooleanSize);
+ }
+ public void Write(bool[] value, BooleanSize booleanSize)
+ {
+ Write(value, 0, value.Length, booleanSize);
+ }
+ public void Write(bool[] value, BooleanSize booleanSize, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length, booleanSize);
+ }
+ public void Write(bool[] value, int startIndex, int count)
+ {
+ Write(value, startIndex, count, BooleanSize);
+ }
+ public void Write(bool[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, BooleanSize);
+ }
+ public void Write(bool[] value, int startIndex, int count, BooleanSize booleanSize)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ for (int i = startIndex; i < count; i++)
+ {
+ Write(value[i], booleanSize);
+ }
+ }
+ public void Write(bool[] value, int startIndex, int count, BooleanSize booleanSize, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, booleanSize);
+ }
+ public void Write(byte value)
+ {
+ SetBufferSize(1);
+ _buffer[0] = value;
+ WriteBytesFromBuffer(1);
+ }
+ public void Write(byte value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(byte[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(byte[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(byte[] value, int startIndex, int count)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ SetBufferSize(count);
+ for (int i = 0; i < count; i++)
+ {
+ _buffer[i] = value[i + startIndex];
+ }
+ WriteBytesFromBuffer(count);
+ }
+ public void Write(byte[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(sbyte value)
+ {
+ SetBufferSize(1);
+ _buffer[0] = (byte)value;
+ WriteBytesFromBuffer(1);
+ }
+ public void Write(sbyte value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(sbyte[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(sbyte[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(sbyte[] value, int startIndex, int count)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ SetBufferSize(count);
+ for (int i = 0; i < count; i++)
+ {
+ _buffer[i] = (byte)value[i + startIndex];
+ }
+ WriteBytesFromBuffer(count);
+ }
+ public void Write(sbyte[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(char value)
+ {
+ Write(value, Encoding);
+ }
+ public void Write(char value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, Encoding);
+ }
+ public void Write(char value, Encoding encoding)
+ {
+ Utils.ThrowIfCannotUseEncoding(encoding);
+ _buffer = encoding.GetBytes(new[] { value });
+ WriteBytesFromBuffer(_buffer.Length);
+ }
+ public void Write(char value, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, encoding);
+ }
+ public void Write(char[] value)
+ {
+ Write(value, 0, value.Length, Encoding);
+ }
+ public void Write(char[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length, Encoding);
+ }
+ public void Write(char[] value, Encoding encoding)
+ {
+ Write(value, 0, value.Length, encoding);
+ }
+ public void Write(char[] value, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length, encoding);
+ }
+ public void Write(char[] value, int startIndex, int count)
+ {
+ Write(value, startIndex, count, Encoding);
+ }
+ public void Write(char[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, Encoding);
+ }
+ public void Write(char[] value, int startIndex, int count, Encoding encoding)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ Utils.ThrowIfCannotUseEncoding(encoding);
+ _buffer = encoding.GetBytes(value, startIndex, count);
+ WriteBytesFromBuffer(_buffer.Length);
+ }
+ public void Write(char[] value, int startIndex, int count, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, encoding);
+ }
+ public void Write(string value, bool nullTerminated)
+ {
+ Write(value, nullTerminated, Encoding);
+ }
+ public void Write(string value, bool nullTerminated, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, nullTerminated, Encoding);
+ }
+ public void Write(string value, bool nullTerminated, Encoding encoding)
+ {
+ Write(value.ToCharArray(), encoding);
+ if (nullTerminated)
+ {
+ Write('\0', encoding);
+ }
+ }
+ public void Write(string value, bool nullTerminated, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, nullTerminated, encoding);
+ }
+ public void Write(string value, int charCount)
+ {
+ Write(value, charCount, Encoding);
+ }
+ public void Write(string value, int charCount, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, charCount, Encoding);
+ }
+ public void Write(string value, int charCount, Encoding encoding)
+ {
+ Utils.TruncateString(value, charCount, out char[] chars);
+ Write(chars, encoding);
+ }
+ public void Write(string value, int charCount, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, charCount, encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, bool nullTerminated)
+ {
+ Write(value, startIndex, count, nullTerminated, Encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, bool nullTerminated, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, nullTerminated, Encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, bool nullTerminated, Encoding encoding)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ for (int i = 0; i < count; i++)
+ {
+ Write(value[i + startIndex], nullTerminated, encoding);
+ }
+ }
+ public void Write(string[] value, int startIndex, int count, bool nullTerminated, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, nullTerminated, encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, int charCount)
+ {
+ Write(value, startIndex, count, charCount, Encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, int charCount, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, charCount, Encoding);
+ }
+ public void Write(string[] value, int startIndex, int count, int charCount, Encoding encoding)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ for (int i = 0; i < count; i++)
+ {
+ Write(value[i + startIndex], charCount, encoding);
+ }
+ }
+ public void Write(string[] value, int startIndex, int count, int charCount, Encoding encoding, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count, charCount, encoding);
+ }
+ public void Write(short value)
+ {
+ _buffer = EndianBitConverter.Int16ToBytes(value, Endianness);
+ WriteBytesFromBuffer(2);
+ }
+ public void Write(short value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(short[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(short[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(short[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.Int16sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 2);
+ }
+ public void Write(short[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(ushort value)
+ {
+ _buffer = EndianBitConverter.Int16ToBytes((short)value, Endianness);
+ WriteBytesFromBuffer(2);
+ }
+ public void Write(ushort value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(ushort[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(ushort[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(ushort[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.UInt16sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 2);
+ }
+ public void Write(ushort[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(int value)
+ {
+ _buffer = EndianBitConverter.Int32ToBytes(value, Endianness);
+ WriteBytesFromBuffer(4);
+ }
+ public void Write(int value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(int[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(int[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(int[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.Int32sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 4);
+ }
+ public void Write(int[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(uint value)
+ {
+ _buffer = EndianBitConverter.Int32ToBytes((int)value, Endianness);
+ WriteBytesFromBuffer(4);
+ }
+ public void Write(uint value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(uint[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(uint[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(uint[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.UInt32sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 4);
+ }
+ public void Write(uint[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(long value)
+ {
+ _buffer = EndianBitConverter.Int64ToBytes(value, Endianness);
+ WriteBytesFromBuffer(8);
+ }
+ public void Write(long value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(long[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(long[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(long[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.Int64sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 8);
+ }
+ public void Write(long[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(ulong value)
+ {
+ _buffer = EndianBitConverter.Int64ToBytes((long)value, Endianness);
+ WriteBytesFromBuffer(8);
+ }
+ public void Write(ulong value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(ulong[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(ulong[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(ulong[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.UInt64sToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 8);
+ }
+ public void Write(ulong[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(float value)
+ {
+ _buffer = EndianBitConverter.SingleToBytes(value, Endianness);
+ WriteBytesFromBuffer(4);
+ }
+ public void Write(float value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(float[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(float[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(float[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.SinglesToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 4);
+ }
+ public void Write(float[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(double value)
+ {
+ _buffer = EndianBitConverter.DoubleToBytes(value, Endianness);
+ WriteBytesFromBuffer(8);
+ }
+ public void Write(double value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(double[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(double[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(double[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.DoublesToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 8);
+ }
+ public void Write(double[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+ public void Write(decimal value)
+ {
+ _buffer = EndianBitConverter.DecimalToBytes(value, Endianness);
+ WriteBytesFromBuffer(16);
+ }
+ public void Write(decimal value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(decimal[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(decimal[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(decimal[] value, int startIndex, int count)
+ {
+ _buffer = EndianBitConverter.DecimalsToBytes(value, startIndex, count, Endianness);
+ WriteBytesFromBuffer(count * 16);
+ }
+ public void Write(decimal[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+
+ // #13 - Handle "Enum" abstract type so we get the correct type in that case
+ // For example, writer.Write((Enum)Enum.Parse(enumType, value))
+ // No "struct" restriction on writes
+ public void Write(TEnum value) where TEnum : Enum
+ {
+ Type underlyingType = Enum.GetUnderlyingType(value.GetType());
+ switch (Type.GetTypeCode(underlyingType))
+ {
+ case TypeCode.Byte: Write(Convert.ToByte(value)); break;
+ case TypeCode.SByte: Write(Convert.ToSByte(value)); break;
+ case TypeCode.Int16: Write(Convert.ToInt16(value)); break;
+ case TypeCode.UInt16: Write(Convert.ToUInt16(value)); break;
+ case TypeCode.Int32: Write(Convert.ToInt32(value)); break;
+ case TypeCode.UInt32: Write(Convert.ToUInt32(value)); break;
+ case TypeCode.Int64: Write(Convert.ToInt64(value)); break;
+ case TypeCode.UInt64: Write(Convert.ToUInt64(value)); break;
+ default: throw new ArgumentOutOfRangeException(nameof(underlyingType));
+ }
+ }
+ public void Write(TEnum value, long offset) where TEnum : Enum
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(TEnum[] value) where TEnum : Enum
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(TEnum[] value, long offset) where TEnum : Enum
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(TEnum[] value, int startIndex, int count) where TEnum : Enum
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ for (int i = 0; i < count; i++)
+ {
+ Write(value[i + startIndex]);
+ }
+ }
+ public void Write(TEnum[] value, int startIndex, int count, long offset) where TEnum : Enum
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+
+ public void Write(DateTime value)
+ {
+ Write(value.ToBinary());
+ }
+ public void Write(DateTime value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value);
+ }
+ public void Write(DateTime[] value)
+ {
+ Write(value, 0, value.Length);
+ }
+ public void Write(DateTime[] value, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, 0, value.Length);
+ }
+ public void Write(DateTime[] value, int startIndex, int count)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return;
+ }
+ for (int i = 0; i < count; i++)
+ {
+ Write(value[i + startIndex]);
+ }
+ }
+ public void Write(DateTime[] value, int startIndex, int count, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(value, startIndex, count);
+ }
+
+ public void Write(IBinarySerializable obj)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ obj.Write(this);
+ }
+ public void Write(IBinarySerializable obj, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(obj);
+ }
+ public void Write(object obj)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+ if (obj is IBinarySerializable bs)
+ {
+ bs.Write(this);
+ return;
+ }
+
+ Type objType = obj.GetType();
+ Utils.ThrowIfCannotReadWriteType(objType);
+
+ // Get public non-static properties
+ foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
+ {
+ if (Utils.AttributeValueOrDefault(propertyInfo, false))
+ {
+ continue; // Skip properties with BinaryIgnoreAttribute
+ }
+
+ Type propertyType = propertyInfo.PropertyType;
+ object value = propertyInfo.GetValue(obj);
+
+ if (propertyType.IsArray)
+ {
+ int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo);
+ if (arrayLength != 0) // Do not need to do anything for length 0
+ {
+ // Get array type
+ Type elementType = propertyType.GetElementType();
+ if (elementType.IsEnum)
+ {
+ elementType = Enum.GetUnderlyingType(elementType);
+ }
+ switch (Type.GetTypeCode(elementType))
+ {
+ case TypeCode.Boolean:
+ {
+ BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize);
+ Write((bool[])value, 0, arrayLength, booleanSize);
+ break;
+ }
+ case TypeCode.Byte: Write((byte[])value, 0, arrayLength); break;
+ case TypeCode.SByte: Write((sbyte[])value, 0, arrayLength); break;
+ case TypeCode.Char:
+ {
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ Write((char[])value, 0, arrayLength, encoding);
+ break;
+ }
+ case TypeCode.Int16: Write((short[])value, 0, arrayLength); break;
+ case TypeCode.UInt16: Write((ushort[])value, 0, arrayLength); break;
+ case TypeCode.Int32: Write((int[])value, 0, arrayLength); break;
+ case TypeCode.UInt32: Write((uint[])value, 0, arrayLength); break;
+ case TypeCode.Int64: Write((long[])value, 0, arrayLength); break;
+ case TypeCode.UInt64: Write((ulong[])value, 0, arrayLength); break;
+ case TypeCode.Single: Write((float[])value, 0, arrayLength); break;
+ case TypeCode.Double: Write((double[])value, 0, arrayLength); break;
+ case TypeCode.Decimal: Write((decimal[])value, 0, arrayLength); break;
+ case TypeCode.DateTime: Write((DateTime[])value, 0, arrayLength); break;
+ case TypeCode.String:
+ {
+ Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength);
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ if (nullTerminated.HasValue)
+ {
+ Write((string[])value, 0, arrayLength, nullTerminated.Value, encoding);
+ }
+ else
+ {
+ Write((string[])value, 0, arrayLength, stringLength, encoding);
+ }
+ break;
+ }
+ case TypeCode.Object:
+ {
+ if (typeof(IBinarySerializable).IsAssignableFrom(elementType))
+ {
+ for (int i = 0; i < arrayLength; i++)
+ {
+ var serializable = (IBinarySerializable)((Array)value).GetValue(i);
+ serializable.Write(this);
+ }
+ }
+ else // Element's type is not supported so try to write the array's objects
+ {
+ for (int i = 0; i < arrayLength; i++)
+ {
+ object elementObj = ((Array)value).GetValue(i);
+ Write(elementObj);
+ }
+ }
+ break;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(elementType));
+ }
+ }
+ }
+ else
+ {
+ if (propertyType.IsEnum)
+ {
+ propertyType = Enum.GetUnderlyingType(propertyType);
+ }
+ switch (Type.GetTypeCode(propertyType))
+ {
+ case TypeCode.Boolean:
+ {
+ BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize);
+ Write((bool)value, booleanSize);
+ break;
+ }
+ case TypeCode.Byte: Write((byte)value); break;
+ case TypeCode.SByte: Write((sbyte)value); break;
+ case TypeCode.Char:
+ {
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ Write((char)value, encoding);
+ break;
+ }
+ case TypeCode.Int16: Write((short)value); break;
+ case TypeCode.UInt16: Write((ushort)value); break;
+ case TypeCode.Int32: Write((int)value); break;
+ case TypeCode.UInt32: Write((uint)value); break;
+ case TypeCode.Int64: Write((long)value); break;
+ case TypeCode.UInt64: Write((ulong)value); break;
+ case TypeCode.Single: Write((float)value); break;
+ case TypeCode.Double: Write((double)value); break;
+ case TypeCode.Decimal: Write((decimal)value); break;
+ case TypeCode.DateTime: Write((DateTime)value); break;
+ case TypeCode.String:
+ {
+ Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength);
+ Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding);
+ if (nullTerminated.HasValue)
+ {
+ Write((string)value, nullTerminated.Value, encoding);
+ }
+ else
+ {
+ Write((string)value, stringLength, encoding);
+ }
+ break;
+ }
+ case TypeCode.Object:
+ {
+ if (typeof(IBinarySerializable).IsAssignableFrom(propertyType))
+ {
+ ((IBinarySerializable)value).Write(this);
+ }
+ else // property's type is not supported so try to write the object
+ {
+ Write(value);
+ }
+ break;
+ }
+ default: throw new ArgumentOutOfRangeException(nameof(propertyType));
+ }
+ }
+ }
+ }
+ public void Write(object obj, long offset)
+ {
+ BaseStream.Position = offset;
+ Write(obj);
+ }
+ }
+}
diff --git a/EndianBinaryIO/EndianBitConverter.cs b/EndianBinaryIO/EndianBitConverter.cs
new file mode 100644
index 0000000..cb00743
--- /dev/null
+++ b/EndianBinaryIO/EndianBitConverter.cs
@@ -0,0 +1,587 @@
+using System;
+
+namespace Kermalis.EndianBinaryIO
+{
+ public static class EndianBitConverter
+ {
+ public static Endianness SystemEndianness { get; } = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian;
+
+ public static unsafe byte[] Int16ToBytes(short value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[2];
+ fixed (byte* b = bytes)
+ {
+ *(short*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 2);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] Int16sToBytes(short[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[2 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((short*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 2);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] UInt16sToBytes(ushort[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[2 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((ushort*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 2);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] Int32ToBytes(int value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[4];
+ fixed (byte* b = bytes)
+ {
+ *(int*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 4);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] Int32sToBytes(int[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[4 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((int*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 4);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] UInt32sToBytes(uint[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[4 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((uint*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 4);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] Int64ToBytes(long value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[8];
+ fixed (byte* b = bytes)
+ {
+ *(long*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 8);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] Int64sToBytes(long[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[8 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((long*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 8);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] UInt64sToBytes(ulong[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[8 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((ulong*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 8);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] SingleToBytes(float value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[4];
+ fixed (byte* b = bytes)
+ {
+ *(float*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 4);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] SinglesToBytes(float[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[4 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((float*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 4);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] DoubleToBytes(double value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[8];
+ fixed (byte* b = bytes)
+ {
+ *(double*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 8);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] DoublesToBytes(double[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[8 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((double*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 8);
+ }
+ }
+ return array;
+ }
+ public static unsafe byte[] DecimalToBytes(decimal value, Endianness targetEndianness)
+ {
+ byte[] bytes = new byte[16];
+ fixed (byte* b = bytes)
+ {
+ *(decimal*)b = value;
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(bytes, 0, 1, 16);
+ }
+ return bytes;
+ }
+ public static unsafe byte[] DecimalsToBytes(decimal[] value, int startIndex, int count, Endianness targetEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out byte[] array))
+ {
+ array = new byte[16 * count];
+ fixed (byte* b = array)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ ((decimal*)b)[i] = value[startIndex + i];
+ }
+ }
+ if (SystemEndianness != targetEndianness)
+ {
+ FlipPrimitives(array, 0, count, 16);
+ }
+ }
+ return array;
+ }
+
+ public static unsafe short BytesToInt16(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 2);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(short*)b;
+ }
+ }
+ public static unsafe short[] BytesToInt16s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 2))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out short[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 2);
+ }
+ array = new short[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((short*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe ushort[] BytesToUInt16s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 2))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out ushort[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 2);
+ }
+ array = new ushort[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((ushort*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe int BytesToInt32(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 4);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(int*)b;
+ }
+ }
+ public static unsafe int[] BytesToInt32s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out int[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 4);
+ }
+ array = new int[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((int*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe uint[] BytesToUInt32s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out uint[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 4);
+ }
+ array = new uint[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((uint*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe long BytesToInt64(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 8);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(long*)b;
+ }
+ }
+ public static unsafe long[] BytesToInt64s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out long[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 8);
+ }
+ array = new long[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((long*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe ulong[] BytesToUInt64s(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out ulong[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 8);
+ }
+ array = new ulong[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((ulong*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe float BytesToSingle(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 4);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(float*)b;
+ }
+ }
+ public static unsafe float[] BytesToSingles(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out float[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 4);
+ }
+ array = new float[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((float*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe double BytesToDouble(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 8);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(double*)b;
+ }
+ }
+ public static unsafe double[] BytesToDoubles(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out double[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 8);
+ }
+ array = new double[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((double*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+ public static unsafe decimal BytesToDecimal(byte[] value, int startIndex, Endianness sourceEndianness)
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, 1, 16);
+ }
+ fixed (byte* b = &value[startIndex])
+ {
+ return *(decimal*)b;
+ }
+ }
+ public static unsafe decimal[] BytesToDecimals(byte[] value, int startIndex, int count, Endianness sourceEndianness)
+ {
+ if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 16))
+ {
+ return Array.Empty();
+ }
+ if (!Utils.ValidateReadArraySize(count, out decimal[] array))
+ {
+ if (SystemEndianness != sourceEndianness)
+ {
+ FlipPrimitives(value, startIndex, count, 16);
+ }
+ array = new decimal[count];
+ fixed (byte* b = &value[startIndex])
+ {
+ for (int i = 0; i < count; i++)
+ {
+ array[i] = ((decimal*)b)[i];
+ }
+ }
+ }
+ return array;
+ }
+
+ private static void FlipPrimitives(byte[] buffer, int startIndex, int primitiveCount, int primitiveSize)
+ {
+ int byteCount = primitiveCount * primitiveSize;
+ for (int i = startIndex; i < byteCount + startIndex; i += primitiveSize)
+ {
+ int a = i;
+ int b = i + primitiveSize - 1;
+ while (a < b)
+ {
+ byte by = buffer[a];
+ buffer[a] = buffer[b];
+ buffer[b] = by;
+ a++;
+ b--;
+ }
+ }
+ }
+ }
+}
diff --git a/EndianBinaryIO/Enums.cs b/EndianBinaryIO/Enums.cs
new file mode 100644
index 0000000..5a93b91
--- /dev/null
+++ b/EndianBinaryIO/Enums.cs
@@ -0,0 +1,18 @@
+namespace Kermalis.EndianBinaryIO
+{
+ public enum BooleanSize : byte
+ {
+ U8,
+ U16,
+ U32,
+ MAX
+ }
+
+ public enum Endianness : byte
+ {
+ LittleEndian,
+ BigEndian,
+ MAX
+ }
+}
+
diff --git a/EndianBinaryIO/IBinarySerializable.cs b/EndianBinaryIO/IBinarySerializable.cs
new file mode 100644
index 0000000..69cbfcf
--- /dev/null
+++ b/EndianBinaryIO/IBinarySerializable.cs
@@ -0,0 +1,8 @@
+namespace Kermalis.EndianBinaryIO
+{
+ public interface IBinarySerializable
+ {
+ void Read(EndianBinaryReader r);
+ void Write(EndianBinaryWriter w);
+ }
+}
diff --git a/EndianBinaryIO/Utils.cs b/EndianBinaryIO/Utils.cs
new file mode 100644
index 0000000..bdaa7aa
--- /dev/null
+++ b/EndianBinaryIO/Utils.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Reflection;
+using System.Text;
+
+namespace Kermalis.EndianBinaryIO
+{
+ internal sealed class Utils
+ {
+ public static void TruncateString(string str, int charCount, out char[] toArray)
+ {
+ toArray = new char[charCount];
+ int numCharsToCopy = Math.Min(charCount, str.Length);
+ for (int i = 0; i < numCharsToCopy; i++)
+ {
+ toArray[i] = str[i];
+ }
+ }
+
+ public static bool TryGetAttribute(PropertyInfo propertyInfo, out TAttribute attribute) where TAttribute : Attribute
+ {
+ object[] attributes = propertyInfo.GetCustomAttributes(typeof(TAttribute), true);
+ if (attributes.Length == 1)
+ {
+ attribute = (TAttribute)attributes[0];
+ return true;
+ }
+ attribute = null;
+ return false;
+ }
+ public static TValue GetAttributeValue(TAttribute attribute) where TAttribute : Attribute, IBinaryAttribute
+ {
+ return (TValue)typeof(TAttribute).GetProperty(nameof(IBinaryAttribute.Value)).GetValue(attribute);
+ }
+ public static TValue AttributeValueOrDefault(PropertyInfo propertyInfo, TValue defaultValue) where TAttribute : Attribute, IBinaryAttribute
+ {
+ if (TryGetAttribute(propertyInfo, out TAttribute attribute))
+ {
+ return GetAttributeValue(attribute);
+ }
+ return defaultValue;
+ }
+
+ public static void ThrowIfCannotReadWriteType(Type type)
+ {
+ if (type.IsArray || type.IsEnum || type.IsInterface || type.IsPointer || type.IsPrimitive)
+ {
+ throw new ArgumentException(nameof(type), $"Cannot read/write \"{type.FullName}\" objects.");
+ }
+ }
+ public static void ThrowIfCannotUseEncoding(Encoding encoding)
+ {
+ if (encoding is null)
+ {
+ throw new ArgumentNullException(nameof(encoding), "EndianBinaryIO could not read/write chars because an encoding was null; make sure you pass one in properly.");
+ }
+ }
+
+ // Returns true if count is 0
+ public static bool ValidateReadArraySize(int count, out T[] array)
+ {
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException($"Invalid array length ({count})");
+ }
+ if (count == 0)
+ {
+ array = Array.Empty();
+ return true;
+ }
+ array = null;
+ return false;
+ }
+ // Returns true if count is 0
+ public static bool ValidateArrayIndexAndCount(Array array, int startIndex, int count)
+ {
+ if (array is null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException($"Invalid array length ({count})");
+ }
+ if (startIndex + count > array.Length)
+ {
+ throw new ArgumentOutOfRangeException($"Invalid array index + count (StartIndex: {startIndex}, Count: {count}, Array Length: {array.Length})");
+ }
+ return count == 0;
+ }
+
+ public static int GetArrayLength(object obj, Type objType, PropertyInfo propertyInfo)
+ {
+ int Validate(int value)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException($"An array property in \"{objType.FullName}\" has an invalid length attribute ({value})");
+ }
+ return value;
+ }
+
+ if (TryGetAttribute(propertyInfo, out BinaryArrayFixedLengthAttribute fixedLenAttribute))
+ {
+ if (propertyInfo.IsDefined(typeof(BinaryArrayVariableLengthAttribute)))
+ {
+ throw new ArgumentException($"An array property in \"{objType.FullName}\" has two array length attributes. Only one should be provided.");
+ }
+ return Validate(GetAttributeValue(fixedLenAttribute));
+ }
+ if (TryGetAttribute(propertyInfo, out BinaryArrayVariableLengthAttribute varLenAttribute))
+ {
+ string anchorName = GetAttributeValue(varLenAttribute);
+ PropertyInfo anchor = objType.GetProperty(anchorName, BindingFlags.Instance | BindingFlags.Public);
+ if (anchor is null)
+ {
+ throw new MissingMemberException($"An array property in \"{objType.FullName}\" has an invalid {nameof(BinaryArrayVariableLengthAttribute)} ({anchorName}).");
+ }
+ return Validate(Convert.ToInt32(anchor.GetValue(obj)));
+ }
+ throw new MissingMemberException($"An array property in \"{objType.FullName}\" is missing an array length attribute. One should be provided.");
+ }
+ public static void GetStringLength(object obj, Type objType, PropertyInfo propertyInfo, bool forReads, out bool? nullTerminated, out int stringLength)
+ {
+ int Validate(int value)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException($"A string property in \"{objType.FullName}\" has an invalid length attribute ({value})");
+ }
+ return value;
+ }
+
+ if (TryGetAttribute(propertyInfo, out BinaryStringNullTerminatedAttribute nullTermAttribute))
+ {
+ if (propertyInfo.IsDefined(typeof(BinaryStringFixedLengthAttribute)) || propertyInfo.IsDefined(typeof(BinaryStringVariableLengthAttribute)))
+ {
+ throw new ArgumentException($"A string property in \"{objType.FullName}\" has a string length attribute and a {nameof(BinaryStringNullTerminatedAttribute)}; cannot use both.");
+ }
+ if (propertyInfo.IsDefined(typeof(BinaryStringTrimNullTerminatorsAttribute)))
+ {
+ throw new ArgumentException($"A string property in \"{objType.FullName}\" has a {nameof(BinaryStringNullTerminatedAttribute)} and a {nameof(BinaryStringTrimNullTerminatorsAttribute)}; cannot use both.");
+ }
+ bool nt = GetAttributeValue(nullTermAttribute);
+ if (forReads && !nt) // Not forcing BinaryStringNullTerminatedAttribute to be treated as true since you may only write objects without reading them.
+ {
+ throw new ArgumentException($"A string property in \"{objType.FullName}\" has a {nameof(BinaryStringNullTerminatedAttribute)} that's set to false." +
+ $" Must use null termination or provide a string length when reading.");
+ }
+ nullTerminated = nt;
+ stringLength = -1;
+ return;
+ }
+ if (TryGetAttribute(propertyInfo, out BinaryStringFixedLengthAttribute fixedLenAttribute))
+ {
+ if (propertyInfo.IsDefined(typeof(BinaryStringVariableLengthAttribute)))
+ {
+ throw new ArgumentException($"A string property in \"{objType.FullName}\" has two string length attributes. Only one should be provided.");
+ }
+ nullTerminated = null;
+ stringLength = Validate(GetAttributeValue(fixedLenAttribute));
+ return;
+ }
+ if (TryGetAttribute(propertyInfo, out BinaryStringVariableLengthAttribute varLenAttribute))
+ {
+ string anchorName = GetAttributeValue(varLenAttribute);
+ PropertyInfo anchor = objType.GetProperty(anchorName, BindingFlags.Instance | BindingFlags.Public);
+ if (anchor is null)
+ {
+ throw new MissingMemberException($"A string property in \"{objType.FullName}\" has an invalid {nameof(BinaryStringVariableLengthAttribute)} ({anchorName}).");
+ }
+ nullTerminated = null;
+ stringLength = Validate(Convert.ToInt32(anchor.GetValue(obj)));
+ return;
+ }
+ throw new MissingMemberException($"A string property in \"{objType.FullName}\" is missing a string length attribute and has no {nameof(BinaryStringNullTerminatedAttribute)}. One should be provided.");
+ }
+ }
+}
diff --git a/ObjectListView/CellEditing/CellEditKeyEngine.cs b/ObjectListView/CellEditing/CellEditKeyEngine.cs
new file mode 100644
index 0000000..a0d67b6
--- /dev/null
+++ b/ObjectListView/CellEditing/CellEditKeyEngine.cs
@@ -0,0 +1,520 @@
+/*
+ * CellEditKeyEngine - A engine that allows the behaviour of arbitrary keys to be configured
+ *
+ * Author: Phillip Piper
+ * Date: 3-March-2011 10:53 pm
+ *
+ * Change log:
+ * v2.8
+ * 2014-05-30 JPP - When a row is disabled, skip over it when looking for another cell to edit
+ * v2.5
+ * 2012-04-14 JPP - Fixed bug where, on a OLV with only a single editable column, tabbing
+ * to change rows would edit the cell above rather than the cell below
+ * the cell being edited.
+ * 2.5
+ * 2011-03-03 JPP - First version
+ *
+ * Copyright (C) 2011-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Forms;
+using BrightIdeasSoftware;
+
+namespace BrightIdeasSoftware {
+ ///
+ /// Indicates the behavior of a key when a cell "on the edge" is being edited.
+ /// and the normal behavior of that key would exceed the edge. For example,
+ /// for a key that normally moves one column to the left, the "edge" would be
+ /// the left most column, since the normal action of the key cannot be taken
+ /// (since there are no more columns to the left).
+ ///
+ public enum CellEditAtEdgeBehaviour {
+ ///
+ /// The key press will be ignored
+ ///
+ Ignore,
+
+ ///
+ /// The key press will result in the cell editing wrapping to the
+ /// cell on the opposite edge.
+ ///
+ Wrap,
+
+ ///
+ /// The key press will wrap, but the column will be changed to the
+ /// appropriate adjacent column. This only makes sense for keys where
+ /// the normal action is ChangeRow.
+ ///
+ ChangeColumn,
+
+ ///
+ /// The key press will wrap, but the row will be changed to the
+ /// appropriate adjacent row. This only makes sense for keys where
+ /// the normal action is ChangeColumn.
+ ///
+ ChangeRow,
+
+ ///
+ /// The key will result in the current edit operation being ended.
+ ///
+ EndEdit
+ };
+
+ ///
+ /// Indicates the normal behaviour of a key when used during a cell edit
+ /// operation.
+ ///
+ public enum CellEditCharacterBehaviour {
+ ///
+ /// The key press will be ignored
+ ///
+ Ignore,
+
+ ///
+ /// The key press will end the current edit and begin an edit
+ /// operation on the next editable cell to the left.
+ ///
+ ChangeColumnLeft,
+
+ ///
+ /// The key press will end the current edit and begin an edit
+ /// operation on the next editable cell to the right.
+ ///
+ ChangeColumnRight,
+
+ ///
+ /// The key press will end the current edit and begin an edit
+ /// operation on the row above.
+ ///
+ ChangeRowUp,
+
+ ///
+ /// The key press will end the current edit and begin an edit
+ /// operation on the row below
+ ///
+ ChangeRowDown,
+
+ ///
+ /// The key press will cancel the current edit
+ ///
+ CancelEdit,
+
+ ///
+ /// The key press will finish the current edit operation
+ ///
+ EndEdit,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb1,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb2,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb3,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb4,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb5,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb6,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb7,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb8,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb9,
+
+ ///
+ /// Custom verb that can be used for specialized actions.
+ ///
+ CustomVerb10,
+ };
+
+ ///
+ /// Instances of this class handle key presses during a cell edit operation.
+ ///
+ public class CellEditKeyEngine {
+
+ #region Public interface
+
+ ///
+ /// Sets the behaviour of a given key
+ ///
+ ///
+ ///
+ ///
+ public virtual void SetKeyBehaviour(Keys key, CellEditCharacterBehaviour normalBehaviour, CellEditAtEdgeBehaviour atEdgeBehaviour) {
+ this.CellEditKeyMap[key] = normalBehaviour;
+ this.CellEditKeyAtEdgeBehaviourMap[key] = atEdgeBehaviour;
+ }
+
+ ///
+ /// Handle a key press
+ ///
+ ///
+ ///
+ /// True if the key was completely handled.
+ public virtual bool HandleKey(ObjectListView olv, Keys keyData) {
+ if (olv == null) throw new ArgumentNullException("olv");
+
+ CellEditCharacterBehaviour behaviour;
+ if (!CellEditKeyMap.TryGetValue(keyData, out behaviour))
+ return false;
+
+ this.ListView = olv;
+
+ switch (behaviour) {
+ case CellEditCharacterBehaviour.Ignore:
+ break;
+ case CellEditCharacterBehaviour.CancelEdit:
+ this.HandleCancelEdit();
+ break;
+ case CellEditCharacterBehaviour.EndEdit:
+ this.HandleEndEdit();
+ break;
+ case CellEditCharacterBehaviour.ChangeColumnLeft:
+ case CellEditCharacterBehaviour.ChangeColumnRight:
+ this.HandleColumnChange(keyData, behaviour);
+ break;
+ case CellEditCharacterBehaviour.ChangeRowDown:
+ case CellEditCharacterBehaviour.ChangeRowUp:
+ this.HandleRowChange(keyData, behaviour);
+ break;
+ default:
+ return this.HandleCustomVerb(keyData, behaviour);
+ };
+
+ return true;
+ }
+
+ #endregion
+
+ #region Implementation properties
+
+ ///
+ /// Gets or sets the ObjectListView on which the current key is being handled.
+ /// This cannot be null.
+ ///
+ protected ObjectListView ListView {
+ get { return listView; }
+ set { listView = value; }
+ }
+ private ObjectListView listView;
+
+ ///
+ /// Gets the row of the cell that is currently being edited
+ ///
+ protected OLVListItem ItemBeingEdited {
+ get {
+ return (this.ListView == null || this.ListView.CellEditEventArgs == null) ? null : this.ListView.CellEditEventArgs.ListViewItem;
+ }
+ }
+
+ ///
+ /// Gets the index of the column of the cell that is being edited
+ ///
+ protected int SubItemIndexBeingEdited {
+ get {
+ return (this.ListView == null || this.ListView.CellEditEventArgs == null) ? -1 : this.ListView.CellEditEventArgs.SubItemIndex;
+ }
+ }
+
+ ///
+ /// Gets or sets the map that remembers the normal behaviour of keys
+ ///
+ protected IDictionary CellEditKeyMap {
+ get {
+ if (cellEditKeyMap == null)
+ this.InitializeCellEditKeyMaps();
+ return cellEditKeyMap;
+ }
+ set {
+ cellEditKeyMap = value;
+ }
+ }
+ private IDictionary cellEditKeyMap;
+
+ ///
+ /// Gets or sets the map that remembers the desired behaviour of keys
+ /// on edge cases.
+ ///
+ protected IDictionary CellEditKeyAtEdgeBehaviourMap {
+ get {
+ if (cellEditKeyAtEdgeBehaviourMap == null)
+ this.InitializeCellEditKeyMaps();
+ return cellEditKeyAtEdgeBehaviourMap;
+ }
+ set {
+ cellEditKeyAtEdgeBehaviourMap = value;
+ }
+ }
+ private IDictionary cellEditKeyAtEdgeBehaviourMap;
+
+ #endregion
+
+ #region Initialization
+
+ ///
+ /// Setup the default key mapping
+ ///
+ protected virtual void InitializeCellEditKeyMaps() {
+ this.cellEditKeyMap = new Dictionary();
+ this.cellEditKeyMap[Keys.Escape] = CellEditCharacterBehaviour.CancelEdit;
+ this.cellEditKeyMap[Keys.Return] = CellEditCharacterBehaviour.EndEdit;
+ this.cellEditKeyMap[Keys.Enter] = CellEditCharacterBehaviour.EndEdit;
+ this.cellEditKeyMap[Keys.Tab] = CellEditCharacterBehaviour.ChangeColumnRight;
+ this.cellEditKeyMap[Keys.Tab | Keys.Shift] = CellEditCharacterBehaviour.ChangeColumnLeft;
+ this.cellEditKeyMap[Keys.Left | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnLeft;
+ this.cellEditKeyMap[Keys.Right | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnRight;
+ this.cellEditKeyMap[Keys.Up | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowUp;
+ this.cellEditKeyMap[Keys.Down | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowDown;
+
+ this.cellEditKeyAtEdgeBehaviourMap = new Dictionary();
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab] = CellEditAtEdgeBehaviour.Wrap;
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab | Keys.Shift] = CellEditAtEdgeBehaviour.Wrap;
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Left | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap;
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Right | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap;
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Up | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn;
+ this.cellEditKeyAtEdgeBehaviourMap[Keys.Down | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn;
+ }
+
+ #endregion
+
+ #region Command handling
+
+ ///
+ /// Handle the end edit command
+ ///
+ protected virtual void HandleEndEdit() {
+ this.ListView.PossibleFinishCellEditing();
+ }
+
+ ///
+ /// Handle the cancel edit command
+ ///
+ protected virtual void HandleCancelEdit() {
+ this.ListView.CancelCellEdit();
+ }
+
+ ///
+ /// Placeholder that subclasses can override to handle any custom verbs
+ ///
+ ///
+ ///
+ ///
+ protected virtual bool HandleCustomVerb(Keys keyData, CellEditCharacterBehaviour behaviour) {
+ return false;
+ }
+
+ ///
+ /// Handle a change row command
+ ///
+ ///
+ ///
+ protected virtual void HandleRowChange(Keys keyData, CellEditCharacterBehaviour behaviour) {
+ // If we couldn't finish editing the current cell, don't try to move it
+ if (!this.ListView.PossibleFinishCellEditing())
+ return;
+
+ OLVListItem olvi = this.ItemBeingEdited;
+ int subItemIndex = this.SubItemIndexBeingEdited;
+ bool isGoingUp = behaviour == CellEditCharacterBehaviour.ChangeRowUp;
+
+ // Try to find a row above (or below) the currently edited cell
+ // If we find one, start editing it and we're done.
+ OLVListItem adjacentOlvi = this.GetAdjacentItemOrNull(olvi, isGoingUp);
+ if (adjacentOlvi != null) {
+ this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex);
+ return;
+ }
+
+ // There is no adjacent row in the direction we want, so we must be on an edge.
+ CellEditAtEdgeBehaviour atEdgeBehaviour;
+ if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour))
+ atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap;
+ switch (atEdgeBehaviour) {
+ case CellEditAtEdgeBehaviour.Ignore:
+ break;
+ case CellEditAtEdgeBehaviour.EndEdit:
+ this.ListView.PossibleFinishCellEditing();
+ break;
+ case CellEditAtEdgeBehaviour.Wrap:
+ adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp);
+ this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex);
+ break;
+ case CellEditAtEdgeBehaviour.ChangeColumn:
+ // Figure out the next editable column
+ List editableColumnsInDisplayOrder = this.EditableColumnsInDisplayOrder;
+ int displayIndex = Math.Max(0, editableColumnsInDisplayOrder.IndexOf(this.ListView.GetColumn(subItemIndex)));
+ if (isGoingUp)
+ displayIndex = (editableColumnsInDisplayOrder.Count + displayIndex - 1) % editableColumnsInDisplayOrder.Count;
+ else
+ displayIndex = (displayIndex + 1) % editableColumnsInDisplayOrder.Count;
+ subItemIndex = editableColumnsInDisplayOrder[displayIndex].Index;
+
+ // Wrap to the next row and start the cell edit
+ adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp);
+ this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex);
+ break;
+ }
+ }
+
+ ///
+ /// Handle a change column command
+ ///
+ ///
+ ///
+ protected virtual void HandleColumnChange(Keys keyData, CellEditCharacterBehaviour behaviour)
+ {
+ // If we couldn't finish editing the current cell, don't try to move it
+ if (!this.ListView.PossibleFinishCellEditing())
+ return;
+
+ // Changing columns only works in details mode
+ if (this.ListView.View != View.Details)
+ return;
+
+ List editableColumns = this.EditableColumnsInDisplayOrder;
+ OLVListItem olvi = this.ItemBeingEdited;
+ int displayIndex = Math.Max(0,
+ editableColumns.IndexOf(this.ListView.GetColumn(this.SubItemIndexBeingEdited)));
+ bool isGoingLeft = behaviour == CellEditCharacterBehaviour.ChangeColumnLeft;
+
+ // Are we trying to continue past one of the edges?
+ if ((isGoingLeft && displayIndex == 0) ||
+ (!isGoingLeft && displayIndex == editableColumns.Count - 1))
+ {
+ // Yes, so figure out our at edge behaviour
+ CellEditAtEdgeBehaviour atEdgeBehaviour;
+ if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour))
+ atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap;
+ switch (atEdgeBehaviour)
+ {
+ case CellEditAtEdgeBehaviour.Ignore:
+ return;
+ case CellEditAtEdgeBehaviour.EndEdit:
+ this.HandleEndEdit();
+ return;
+ case CellEditAtEdgeBehaviour.ChangeRow:
+ case CellEditAtEdgeBehaviour.Wrap:
+ if (atEdgeBehaviour == CellEditAtEdgeBehaviour.ChangeRow)
+ olvi = GetAdjacentItem(olvi, isGoingLeft && displayIndex == 0);
+ if (isGoingLeft)
+ displayIndex = editableColumns.Count - 1;
+ else
+ displayIndex = 0;
+ break;
+ }
+ }
+ else
+ {
+ if (isGoingLeft)
+ displayIndex -= 1;
+ else
+ displayIndex += 1;
+ }
+
+ int subItemIndex = editableColumns[displayIndex].Index;
+ this.StartCellEditIfDifferent(olvi, subItemIndex);
+ }
+
+ #endregion
+
+ #region Utilities
+
+ ///
+ /// Start editing the indicated cell if that cell is not already being edited
+ ///
+ /// The row to edit
+ /// The cell within that row to edit
+ protected void StartCellEditIfDifferent(OLVListItem olvi, int subItemIndex) {
+ if (this.ItemBeingEdited == olvi && this.SubItemIndexBeingEdited == subItemIndex)
+ return;
+
+ this.ListView.EnsureVisible(olvi.Index);
+ this.ListView.StartCellEdit(olvi, subItemIndex);
+ }
+
+ ///
+ /// Gets the adjacent item to the given item in the given direction.
+ /// If that item is disabled, continue in that direction until an enabled item is found.
+ ///
+ /// The row whose neighbour is sought
+ /// The direction of the adjacentness
+ /// An OLVListView adjacent to the given item, or null if there are no more enabled items in that direction.
+ protected OLVListItem GetAdjacentItemOrNull(OLVListItem olvi, bool up) {
+ OLVListItem item = up ? this.ListView.GetPreviousItem(olvi) : this.ListView.GetNextItem(olvi);
+ while (item != null && !item.Enabled)
+ item = up ? this.ListView.GetPreviousItem(item) : this.ListView.GetNextItem(item);
+ return item;
+ }
+
+ ///
+ /// Gets the adjacent item to the given item in the given direction, wrapping if needed.
+ ///
+ /// The row whose neighbour is sought
+ /// The direction of the adjacentness
+ /// An OLVListView adjacent to the given item, or null if there are no more items in that direction.
+ protected OLVListItem GetAdjacentItem(OLVListItem olvi, bool up) {
+ return this.GetAdjacentItemOrNull(olvi, up) ?? this.GetAdjacentItemOrNull(null, up);
+ }
+
+ ///
+ /// Gets a collection of columns that are editable in the order they are shown to the user
+ ///
+ protected List EditableColumnsInDisplayOrder {
+ get {
+ List editableColumnsInDisplayOrder = new List();
+ foreach (OLVColumn x in this.ListView.ColumnsInDisplayOrder)
+ if (x.IsEditable)
+ editableColumnsInDisplayOrder.Add(x);
+ return editableColumnsInDisplayOrder;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/CellEditing/CellEditors.cs b/ObjectListView/CellEditing/CellEditors.cs
new file mode 100644
index 0000000..4314021
--- /dev/null
+++ b/ObjectListView/CellEditing/CellEditors.cs
@@ -0,0 +1,325 @@
+/*
+ * CellEditors - Several slightly modified controls that are used as cell editors within ObjectListView.
+ *
+ * Author: Phillip Piper
+ * Date: 20/10/2008 5:15 PM
+ *
+ * Change log:
+ * 2018-05-05 JPP - Added ControlUtilities.AutoResizeDropDown()
+ * v2.6
+ * 2012-08-02 JPP - Make most editors public so they can be reused/subclassed
+ * v2.3
+ * 2009-08-13 JPP - Standardized code formatting
+ * v2.2.1
+ * 2008-01-18 JPP - Added special handling for enums
+ * 2008-01-16 JPP - Added EditorRegistry
+ * v2.0.1
+ * 2008-10-20 JPP - Separated from ObjectListView.cs
+ *
+ * Copyright (C) 2006-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// An interface that allows cell editors to specifically handle getting and setting
+ /// values from ObjectListView
+ ///
+ public interface IOlvEditor {
+ object Value { get; set; }
+ }
+
+ public static class ControlUtilities {
+
+ ///
+ /// Configure the given ComboBox so that the dropped down menu is auto-sized to
+ /// be wide enough to show the widest item.
+ ///
+ ///
+ public static void AutoResizeDropDown(ComboBox dropDown) {
+ if (dropDown == null)
+ throw new ArgumentNullException("dropDown");
+
+ dropDown.DropDown += delegate(object sender, EventArgs args) {
+
+ // Calculate the maximum width of the drop down items
+ int newWidth = 0;
+ foreach (object item in dropDown.Items) {
+ newWidth = Math.Max(newWidth, TextRenderer.MeasureText(item.ToString(), dropDown.Font).Width);
+ }
+
+ int vertScrollBarWidth = dropDown.Items.Count > dropDown.MaxDropDownItems ? SystemInformation.VerticalScrollBarWidth : 0;
+ dropDown.DropDownWidth = newWidth + vertScrollBarWidth;
+ };
+ }
+ }
+
+ ///
+ /// These items allow combo boxes to remember a value and its description.
+ ///
+ public class ComboBoxItem
+ {
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ComboBoxItem(Object key, String description) {
+ this.key = key;
+ this.description = description;
+ }
+ private readonly String description;
+
+ ///
+ ///
+ ///
+ public Object Key {
+ get { return key; }
+ }
+ private readonly Object key;
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ ///
+ /// A string that represents the current object.
+ ///
+ /// 2
+ public override string ToString() {
+ return this.description;
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ // Cell editors
+ // These classes are simple cell editors that make it easier to get and set
+ // the value that the control is showing.
+ // In many cases, you can intercept the CellEditStarting event to
+ // change the characteristics of the editor. For example, changing
+ // the acceptable range for a numeric editor or changing the strings
+ // that represent true and false values for a boolean editor.
+
+ ///
+ /// This editor shows and auto completes values from the given listview column.
+ ///
+ [ToolboxItem(false)]
+ public class AutoCompleteCellEditor : ComboBox
+ {
+ ///
+ /// Create an AutoCompleteCellEditor
+ ///
+ ///
+ ///
+ public AutoCompleteCellEditor(ObjectListView lv, OLVColumn column) {
+ this.DropDownStyle = ComboBoxStyle.DropDown;
+
+ Dictionary alreadySeen = new Dictionary();
+ for (int i = 0; i < Math.Min(lv.GetItemCount(), 1000); i++) {
+ String str = column.GetStringValue(lv.GetModelObject(i));
+ if (!alreadySeen.ContainsKey(str)) {
+ this.Items.Add(str);
+ alreadySeen[str] = true;
+ }
+ }
+
+ this.Sorted = true;
+ this.AutoCompleteSource = AutoCompleteSource.ListItems;
+ this.AutoCompleteMode = AutoCompleteMode.Append;
+
+ ControlUtilities.AutoResizeDropDown(this);
+ }
+ }
+
+ ///
+ /// This combo box is specialized to allow editing of an enum.
+ ///
+ [ToolboxItem(false)]
+ public class EnumCellEditor : ComboBox
+ {
+ ///
+ ///
+ ///
+ ///
+ public EnumCellEditor(Type type) {
+ this.DropDownStyle = ComboBoxStyle.DropDownList;
+ this.ValueMember = "Key";
+
+ ArrayList values = new ArrayList();
+ foreach (object value in Enum.GetValues(type))
+ values.Add(new ComboBoxItem(value, Enum.GetName(type, value)));
+
+ this.DataSource = values;
+
+ ControlUtilities.AutoResizeDropDown(this);
+ }
+ }
+
+ ///
+ /// This editor simply shows and edits integer values.
+ ///
+ [ToolboxItem(false)]
+ public class IntUpDown : NumericUpDown
+ {
+ ///
+ ///
+ ///
+ public IntUpDown() {
+ this.DecimalPlaces = 0;
+ this.Minimum = -9999999;
+ this.Maximum = 9999999;
+ }
+
+ ///
+ /// Gets or sets the value shown by this editor
+ ///
+ public new int Value {
+ get { return Decimal.ToInt32(base.Value); }
+ set { base.Value = new Decimal(value); }
+ }
+ }
+
+ ///
+ /// This editor simply shows and edits unsigned integer values.
+ ///
+ /// This class can't be made public because unsigned int is not a
+ /// CLS-compliant type. If you want to use, just copy the code to this class
+ /// into your project and use it from there.
+ [ToolboxItem(false)]
+ internal class UintUpDown : NumericUpDown
+ {
+ public UintUpDown() {
+ this.DecimalPlaces = 0;
+ this.Minimum = 0;
+ this.Maximum = 9999999;
+ }
+
+ public new uint Value {
+ get { return Decimal.ToUInt32(base.Value); }
+ set { base.Value = new Decimal(value); }
+ }
+ }
+
+ ///
+ /// This editor simply shows and edits boolean values.
+ ///
+ [ToolboxItem(false)]
+ public class BooleanCellEditor : ComboBox
+ {
+ ///
+ ///
+ ///
+ public BooleanCellEditor() {
+ this.DropDownStyle = ComboBoxStyle.DropDownList;
+ this.ValueMember = "Key";
+
+ ArrayList values = new ArrayList();
+ values.Add(new ComboBoxItem(false, "False"));
+ values.Add(new ComboBoxItem(true, "True"));
+
+ this.DataSource = values;
+ }
+ }
+
+ ///
+ /// This editor simply shows and edits boolean values using a checkbox
+ ///
+ [ToolboxItem(false)]
+ public class BooleanCellEditor2 : CheckBox
+ {
+ ///
+ /// Gets or sets the value shown by this editor
+ ///
+ public bool? Value {
+ get {
+ switch (this.CheckState) {
+ case CheckState.Checked: return true;
+ case CheckState.Indeterminate: return null;
+ case CheckState.Unchecked:
+ default: return false;
+ }
+ }
+ set {
+ if (value.HasValue)
+ this.CheckState = value.Value ? CheckState.Checked : CheckState.Unchecked;
+ else
+ this.CheckState = CheckState.Indeterminate;
+ }
+ }
+
+ ///
+ /// Gets or sets how the checkbox will be aligned
+ ///
+ public new HorizontalAlignment TextAlign {
+ get {
+ switch (this.CheckAlign) {
+ case ContentAlignment.MiddleRight: return HorizontalAlignment.Right;
+ case ContentAlignment.MiddleCenter: return HorizontalAlignment.Center;
+ case ContentAlignment.MiddleLeft:
+ default: return HorizontalAlignment.Left;
+ }
+ }
+ set {
+ switch (value) {
+ case HorizontalAlignment.Left:
+ this.CheckAlign = ContentAlignment.MiddleLeft;
+ break;
+ case HorizontalAlignment.Center:
+ this.CheckAlign = ContentAlignment.MiddleCenter;
+ break;
+ case HorizontalAlignment.Right:
+ this.CheckAlign = ContentAlignment.MiddleRight;
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// This editor simply shows and edits floating point values.
+ ///
+ /// You can intercept the CellEditStarting event if you want
+ /// to change the characteristics of the editor. For example, by increasing
+ /// the number of decimal places.
+ [ToolboxItem(false)]
+ public class FloatCellEditor : NumericUpDown
+ {
+ ///
+ ///
+ ///
+ public FloatCellEditor() {
+ this.DecimalPlaces = 2;
+ this.Minimum = -9999999;
+ this.Maximum = 9999999;
+ }
+
+ ///
+ /// Gets or sets the value shown by this editor
+ ///
+ public new double Value {
+ get { return Convert.ToDouble(base.Value); }
+ set { base.Value = Convert.ToDecimal(value); }
+ }
+ }
+}
diff --git a/ObjectListView/CellEditing/EditorRegistry.cs b/ObjectListView/CellEditing/EditorRegistry.cs
new file mode 100644
index 0000000..f92854f
--- /dev/null
+++ b/ObjectListView/CellEditing/EditorRegistry.cs
@@ -0,0 +1,213 @@
+/*
+ * EditorRegistry - A registry mapping types to cell editors.
+ *
+ * Author: Phillip Piper
+ * Date: 6-March-2011 7:53 am
+ *
+ * Change log:
+ * 2011-03-31 JPP - Use OLVColumn.DataType if the value to be edited is null
+ * 2011-03-06 JPP - Separated from CellEditors.cs
+ *
+ * Copyright (C) 2011-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Forms;
+using System.Reflection;
+
+namespace BrightIdeasSoftware {
+
+ ///
+ /// A delegate that creates an editor for the given value
+ ///
+ /// The model from which that value came
+ /// The column for which the editor is being created
+ /// A representative value of the type to be edited. This value may not be the exact
+ /// value for the column/model combination. It could be simply representative of
+ /// the appropriate type of value.
+ /// A control which can edit the given value
+ public delegate Control EditorCreatorDelegate(Object model, OLVColumn column, Object value);
+
+ ///
+ /// An editor registry gives a way to decide what cell editor should be used to edit
+ /// the value of a cell. Programmers can register non-standard types and the control that
+ /// should be used to edit instances of that type.
+ ///
+ ///
+ /// All ObjectListViews share the same editor registry.
+ ///
+ public class EditorRegistry {
+ #region Initializing
+
+ ///
+ /// Create an EditorRegistry
+ ///
+ public EditorRegistry() {
+ this.InitializeStandardTypes();
+ }
+
+ private void InitializeStandardTypes() {
+ this.Register(typeof(Boolean), typeof(BooleanCellEditor));
+ this.Register(typeof(Int16), typeof(IntUpDown));
+ this.Register(typeof(Int32), typeof(IntUpDown));
+ this.Register(typeof(Int64), typeof(IntUpDown));
+ this.Register(typeof(UInt16), typeof(UintUpDown));
+ this.Register(typeof(UInt32), typeof(UintUpDown));
+ this.Register(typeof(UInt64), typeof(UintUpDown));
+ this.Register(typeof(Single), typeof(FloatCellEditor));
+ this.Register(typeof(Double), typeof(FloatCellEditor));
+ this.Register(typeof(DateTime), delegate(Object model, OLVColumn column, Object value) {
+ DateTimePicker c = new DateTimePicker();
+ c.Format = DateTimePickerFormat.Short;
+ return c;
+ });
+ this.Register(typeof(Boolean), delegate(Object model, OLVColumn column, Object value) {
+ CheckBox c = new BooleanCellEditor2();
+ c.ThreeState = column.TriStateCheckBoxes;
+ return c;
+ });
+ }
+
+ #endregion
+
+ #region Registering
+
+ ///
+ /// Register that values of 'type' should be edited by instances of 'controlType'.
+ ///
+ /// The type of value to be edited
+ /// The type of the Control that will edit values of 'type'
+ ///
+ /// ObjectListView.EditorRegistry.Register(typeof(Color), typeof(MySpecialColorEditor));
+ ///
+ public void Register(Type type, Type controlType) {
+ this.Register(type, delegate(Object model, OLVColumn column, Object value) {
+ return controlType.InvokeMember("", BindingFlags.CreateInstance, null, null, null) as Control;
+ });
+ }
+
+ ///
+ /// Register the given delegate so that it is called to create editors
+ /// for values of the given type
+ ///
+ /// The type of value to be edited
+ /// The delegate that will create a control that can edit values of 'type'
+ ///
+ /// ObjectListView.EditorRegistry.Register(typeof(Color), CreateColorEditor);
+ /// ...
+ /// public Control CreateColorEditor(Object model, OLVColumn column, Object value)
+ /// {
+ /// return new MySpecialColorEditor();
+ /// }
+ ///
+ public void Register(Type type, EditorCreatorDelegate creator) {
+ this.creatorMap[type] = creator;
+ }
+
+ ///
+ /// Register a delegate that will be called to create an editor for values
+ /// that have not been handled.
+ ///
+ /// The delegate that will create a editor for all other types
+ public void RegisterDefault(EditorCreatorDelegate creator) {
+ this.defaultCreator = creator;
+ }
+
+ ///
+ /// Register a delegate that will be given a chance to create a control
+ /// before any other option is considered.
+ ///
+ /// The delegate that will create a control
+ public void RegisterFirstChance(EditorCreatorDelegate creator) {
+ this.firstChanceCreator = creator;
+ }
+
+ ///
+ /// Remove the registered handler for the given type
+ ///
+ /// Does nothing if the given type doesn't exist
+ /// The type whose registration is to be removed
+ public void Unregister(Type type) {
+ if (this.creatorMap.ContainsKey(type))
+ this.creatorMap.Remove(type);
+ }
+
+ #endregion
+
+ #region Accessing
+
+ ///
+ /// Create and return an editor that is appropriate for the given value.
+ /// Return null if no appropriate editor can be found.
+ ///
+ /// The model involved
+ /// The column to be edited
+ /// The value to be edited. This value may not be the exact
+ /// value for the column/model combination. It could be simply representative of
+ /// the appropriate type of value.
+ /// A Control that can edit the given type of values
+ public Control GetEditor(Object model, OLVColumn column, Object value) {
+ Control editor;
+
+ // Give the first chance delegate a chance to decide
+ if (this.firstChanceCreator != null) {
+ editor = this.firstChanceCreator(model, column, value);
+ if (editor != null)
+ return editor;
+ }
+
+ // Try to find a creator based on the type of the value (or the column)
+ Type type = value == null ? column.DataType : value.GetType();
+ if (type != null && this.creatorMap.ContainsKey(type)) {
+ editor = this.creatorMap[type](model, column, value);
+ if (editor != null)
+ return editor;
+ }
+
+ // Enums without other processing get a special editor
+ if (value != null && value.GetType().IsEnum)
+ return this.CreateEnumEditor(value.GetType());
+
+ // Give any default creator a final chance
+ if (this.defaultCreator != null)
+ return this.defaultCreator(model, column, value);
+
+ return null;
+ }
+
+ ///
+ /// Create and return an editor that will edit values of the given type
+ ///
+ /// A enum type
+ protected Control CreateEnumEditor(Type type) {
+ return new EnumCellEditor(type);
+ }
+
+ #endregion
+
+ #region Private variables
+
+ private EditorCreatorDelegate firstChanceCreator;
+ private EditorCreatorDelegate defaultCreator;
+ private Dictionary creatorMap = new Dictionary();
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/CustomDictionary.xml b/ObjectListView/CustomDictionary.xml
new file mode 100644
index 0000000..f2cf5b9
--- /dev/null
+++ b/ObjectListView/CustomDictionary.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ br
+ Canceled
+ Center
+ Color
+ Colors
+ f
+ fmt
+ g
+ gdi
+ hti
+ i
+ lightbox
+ lv
+ lvi
+ lvsi
+ m
+ multi
+ Munger
+ n
+ olv
+ olvi
+ p
+ parms
+ r
+ Renderer
+ s
+ SubItem
+ Unapply
+ Unpause
+ x
+ y
+
+
+ ComPlus
+
+
+
+
+ OLV
+
+
+
diff --git a/ObjectListView/DataListView.cs b/ObjectListView/DataListView.cs
new file mode 100644
index 0000000..2961d04
--- /dev/null
+++ b/ObjectListView/DataListView.cs
@@ -0,0 +1,240 @@
+/*
+ * DataListView - A data-bindable listview
+ *
+ * Author: Phillip Piper
+ * Date: 27/09/2008 9:15 AM
+ *
+ * Change log:
+ * 2015-02-02 JPP - Made Unfreezing more efficient by removing a redundant BuildList() call
+ * v2.6
+ * 2011-02-27 JPP - Moved most of the logic to DataSourceAdapter (where it
+ * can be used by FastDataListView too)
+ * v2.3
+ * 2009-01-18 JPP - Boolean columns are now handled as checkboxes
+ * - Auto-generated columns would fail if the data source was
+ * reseated, even to the same data source
+ * v2.0.1
+ * 2009-01-07 JPP - Made all public and protected methods virtual
+ * 2008-10-03 JPP - Separated from ObjectListView.cs
+ *
+ * Copyright (C) 2006-2015 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Data;
+using System.Diagnostics;
+using System.Drawing.Design;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware
+{
+
+ ///
+ /// A DataListView is a ListView that can be bound to a datasource (which would normally be a DataTable or DataView).
+ ///
+ ///
+ /// This listview keeps itself in sync with its source datatable by listening for change events.
+ /// The DataListView will automatically create columns to show all of the data source's columns/properties, if there is not already
+ /// a column showing that property. This allows you to define one or two columns in the designer and then have the others generated automatically.
+ /// If you don't want any column to be auto generated, set to false.
+ /// These generated columns will be only the simplest view of the world, and would look more interesting with a few delegates installed.
+ /// This listview will also automatically generate missing aspect getters to fetch the values from the data view.
+ /// Changing data sources is possible, but error prone. Before changing data sources, the programmer is responsible for modifying/resetting
+ /// the column collection to be valid for the new data source.
+ /// Internally, a CurrencyManager controls keeping the data source in-sync with other users of the data source (as per normal .NET
+ /// behavior). This means that the model objects in the DataListView are DataRowView objects. If you write your own AspectGetters/Setters,
+ /// they will be given DataRowView objects.
+ ///
+ public class DataListView : ObjectListView
+ {
+ #region Life and death
+
+ ///
+ /// Make a DataListView
+ ///
+ public DataListView()
+ {
+ this.Adapter = new DataSourceAdapter(this);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ protected override void Dispose(bool disposing) {
+ this.Adapter.Dispose();
+ base.Dispose(disposing);
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets or sets whether or not columns will be automatically generated to show the
+ /// columns when the DataSource is set.
+ ///
+ /// This must be set before the DataSource is set. It has no effect afterwards.
+ [Category("Data"),
+ Description("Should the control automatically generate columns from the DataSource"),
+ DefaultValue(true)]
+ public bool AutoGenerateColumns {
+ get { return this.Adapter.AutoGenerateColumns; }
+ set { this.Adapter.AutoGenerateColumns = value; }
+ }
+
+ ///
+ /// Get or set the DataSource that will be displayed in this list view.
+ ///
+ /// The DataSource should implement either , ,
+ /// or . Some common examples are the following types of objects:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// When binding to a list container (i.e. one that implements the
+ /// interface, such as )
+ /// you must also set the property in order
+ /// to identify which particular list you would like to display. You
+ /// may also set the property even when
+ /// DataSource refers to a list, since can
+ /// also be used to navigate relations between lists.
+ /// When a DataSource is set, the control will create OLVColumns to show any
+ /// data source columns that are not already shown.
+ /// If the DataSource is changed, you will have to remove any previously
+ /// created columns, since they will be configured for the previous DataSource.
+ /// .
+ ///
+ [Category("Data"),
+ TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")]
+ public virtual Object DataSource
+ {
+ get { return this.Adapter.DataSource; }
+ set { this.Adapter.DataSource = value; }
+ }
+
+ ///
+ /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data.
+ ///
+ /// If the data source is not a DataSet or DataViewManager, this property has no effect
+ [Category("Data"),
+ Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)),
+ DefaultValue("")]
+ public virtual string DataMember
+ {
+ get { return this.Adapter.DataMember; }
+ set { this.Adapter.DataMember = value; }
+ }
+
+ #endregion
+
+ #region Implementation properties
+
+ ///
+ /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed
+ /// for data binding.
+ ///
+ ///
+ /// Adaptors cannot be shared between controls. Each DataListView needs its own adapter.
+ ///
+ protected DataSourceAdapter Adapter {
+ get {
+ Debug.Assert(adapter != null, "Data adapter should not be null");
+ return adapter;
+ }
+ set { adapter = value; }
+ }
+ private DataSourceAdapter adapter;
+
+ #endregion
+
+ #region Object manipulations
+
+ ///
+ /// Add the given collection of model objects to this control.
+ ///
+ /// A collection of model objects
+ /// This is a no-op for data lists, since the data
+ /// is controlled by the DataSource. Manipulate the data source
+ /// rather than this view of the data source.
+ public override void AddObjects(ICollection modelObjects)
+ {
+ }
+
+ ///
+ /// Insert the given collection of objects before the given position
+ ///
+ /// Where to insert the objects
+ /// The objects to be inserted
+ /// This is a no-op for data lists, since the data
+ /// is controlled by the DataSource. Manipulate the data source
+ /// rather than this view of the data source.
+ public override void InsertObjects(int index, ICollection modelObjects) {
+ }
+
+ ///
+ /// Remove the given collection of model objects from this control.
+ ///
+ /// This is a no-op for data lists, since the data
+ /// is controlled by the DataSource. Manipulate the data source
+ /// rather than this view of the data source.
+ public override void RemoveObjects(ICollection modelObjects)
+ {
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ ///
+ /// Change the Unfreeze behaviour
+ ///
+ protected override void DoUnfreeze() {
+
+ // Copied from base method, but we don't need to BuildList() since we know that our
+ // data adaptor is going to do that immediately after this method exits.
+ this.EndUpdate();
+ this.ResizeFreeSpaceFillingColumns();
+ // this.BuildList();
+ }
+
+ ///
+ /// Handles parent binding context changes
+ ///
+ /// Unused EventArgs.
+ protected override void OnParentBindingContextChanged(EventArgs e)
+ {
+ base.OnParentBindingContextChanged(e);
+
+ // BindingContext is an ambient property - by default it simply picks
+ // up the parent control's context (unless something has explicitly
+ // given us our own). So we must respond to changes in our parent's
+ // binding context in the same way we would changes to our own
+ // binding context.
+
+ // THINK: Do we need to forward this to the adapter?
+ }
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/DataTreeListView.cs b/ObjectListView/DataTreeListView.cs
new file mode 100644
index 0000000..65179a9
--- /dev/null
+++ b/ObjectListView/DataTreeListView.cs
@@ -0,0 +1,240 @@
+/*
+ * DataTreeListView - A data bindable TreeListView
+ *
+ * Author: Phillip Piper
+ * Date: 05/05/2012 3:26 PM
+ *
+ * Change log:
+
+ * 2012-05-05 JPP Initial version
+ *
+ * TO DO:
+
+ *
+ * Copyright (C) 2012 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Data;
+using System.Diagnostics;
+using System.Drawing.Design;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// A DataTreeListView is a TreeListView that calculates its hierarchy based on
+ /// information in the data source.
+ ///
+ ///
+ /// Like a , a DataTreeListView sources all its information
+ /// from a combination of and .
+ /// can be a DataTable, DataSet,
+ /// or anything that implements .
+ ///
+ ///
+ /// To function properly, the DataTreeListView requires:
+ ///
+ /// the table to have a column which holds a unique for the row. The name of this column must be set in .
+ /// the table to have a column which holds id of the hierarchical parent of the row. The name of this column must be set in .
+ /// a value which identifies which rows are the roots of the tree ().
+ ///
+ /// The hierarchy structure is determined finding all the rows where the parent key is equal to . These rows
+ /// become the root objects of the hierarchy.
+ ///
+ /// Like a TreeListView, the hierarchy must not contain cycles. Bad things will happen if the data is cyclic.
+ ///
+ public partial class DataTreeListView : TreeListView
+ {
+ #region Public Properties
+
+ ///
+ /// Gets or sets whether or not columns will be automatically generated to show the
+ /// columns when the DataSource is set.
+ ///
+ /// This must be set before the DataSource is set. It has no effect afterwards.
+ [Category("Data"),
+ Description("Should the control automatically generate columns from the DataSource"),
+ DefaultValue(true)]
+ public bool AutoGenerateColumns
+ {
+ get { return this.Adapter.AutoGenerateColumns; }
+ set { this.Adapter.AutoGenerateColumns = value; }
+ }
+
+ ///
+ /// Get or set the DataSource that will be displayed in this list view.
+ ///
+ /// The DataSource should implement either , ,
+ /// or . Some common examples are the following types of objects:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// When binding to a list container (i.e. one that implements the
+ /// interface, such as )
+ /// you must also set the property in order
+ /// to identify which particular list you would like to display. You
+ /// may also set the property even when
+ /// DataSource refers to a list, since can
+ /// also be used to navigate relations between lists.
+ ///
+ [Category("Data"),
+ TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")]
+ public virtual Object DataSource {
+ get { return this.Adapter.DataSource; }
+ set { this.Adapter.DataSource = value; }
+ }
+
+ ///
+ /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data.
+ ///
+ /// If the data source is not a DataSet or DataViewManager, this property has no effect
+ [Category("Data"),
+ Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)),
+ DefaultValue("")]
+ public virtual string DataMember {
+ get { return this.Adapter.DataMember; }
+ set { this.Adapter.DataMember = value; }
+ }
+
+ ///
+ /// Gets or sets the name of the property/column that uniquely identifies each row.
+ ///
+ ///
+ ///
+ /// The value contained by this column must be unique across all rows
+ /// in the data source. Odd and unpredictable things will happen if two
+ /// rows have the same id.
+ ///
+ /// Null cannot be a valid key value.
+ ///
+ [Category("Data"),
+ Description("The name of the property/column that holds the key of a row"),
+ DefaultValue(null)]
+ public virtual string KeyAspectName {
+ get { return this.Adapter.KeyAspectName; }
+ set { this.Adapter.KeyAspectName = value; }
+ }
+
+ ///
+ /// Gets or sets the name of the property/column that contains the key of
+ /// the parent of a row.
+ ///
+ ///
+ ///
+ /// The test condition for deciding if one row is the parent of another is functionally
+ /// equivalent to this:
+ ///
+ /// Object.Equals(candidateParentRow[this.KeyAspectName], row[this.ParentKeyAspectName])
+ ///
+ ///
+ /// Unlike key value, parent keys can be null but a null parent key can only be used
+ /// to identify root objects.
+ ///
+ [Category("Data"),
+ Description("The name of the property/column that holds the key of the parent of a row"),
+ DefaultValue(null)]
+ public virtual string ParentKeyAspectName {
+ get { return this.Adapter.ParentKeyAspectName; }
+ set { this.Adapter.ParentKeyAspectName = value; }
+ }
+
+ ///
+ /// Gets or sets the value that identifies a row as a root object.
+ /// When the ParentKey of a row equals the RootKeyValue, that row will
+ /// be treated as root of the TreeListView.
+ ///
+ ///
+ ///
+ /// The test condition for deciding a root object is functionally
+ /// equivalent to this:
+ ///
+ /// Object.Equals(candidateRow[this.ParentKeyAspectName], this.RootKeyValue)
+ ///
+ ///
+ /// The RootKeyValue can be null. Actually, it can be any value that can
+ /// be compared for equality against a basic type.
+ /// If this is set to the wrong value (i.e. to a value that no row
+ /// has in the parent id column), the list will be empty.
+ ///
+ [Browsable(false),
+ DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public virtual object RootKeyValue {
+ get { return this.Adapter.RootKeyValue; }
+ set { this.Adapter.RootKeyValue = value; }
+ }
+
+ ///
+ /// Gets or sets the value that identifies a row as a root object.
+ /// . The RootKeyValue can be of any type,
+ /// but the IDE cannot sensibly represent a value of any type,
+ /// so this is a typed wrapper around that property.
+ ///
+ ///
+ /// If you want the root value to be something other than a string,
+ /// you will have set it yourself.
+ ///
+ [Category("Data"),
+ Description("The parent id value that identifies a row as a root object"),
+ DefaultValue(null)]
+ public virtual string RootKeyValueString {
+ get { return Convert.ToString(this.Adapter.RootKeyValue); }
+ set { this.Adapter.RootKeyValue = value; }
+ }
+
+ ///
+ /// Gets or sets whether or not the key columns (id and parent id) should
+ /// be shown to the user.
+ ///
+ /// This must be set before the DataSource is set. It has no effect
+ /// afterwards.
+ [Category("Data"),
+ Description("Should the keys columns (id and parent id) be shown to the user?"),
+ DefaultValue(true)]
+ public virtual bool ShowKeyColumns {
+ get { return this.Adapter.ShowKeyColumns; }
+ set { this.Adapter.ShowKeyColumns = value; }
+ }
+
+ #endregion
+
+ #region Implementation properties
+
+ ///
+ /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed
+ /// for data binding.
+ ///
+ protected TreeDataSourceAdapter Adapter {
+ get {
+ if (this.adapter == null)
+ this.adapter = new TreeDataSourceAdapter(this);
+ return adapter;
+ }
+ set { adapter = value; }
+ }
+ private TreeDataSourceAdapter adapter;
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/DragDrop/DragSource.cs b/ObjectListView/DragDrop/DragSource.cs
new file mode 100644
index 0000000..1abf13b
--- /dev/null
+++ b/ObjectListView/DragDrop/DragSource.cs
@@ -0,0 +1,219 @@
+/*
+ * DragSource.cs - Add drag source functionality to an ObjectListView
+ *
+ * Author: Phillip Piper
+ * Date: 2009-03-17 5:15 PM
+ *
+ * Change log:
+ * 2011-03-29 JPP - Separate OLVDataObject.cs
+ * v2.3
+ * 2009-07-06 JPP - Make sure Link is acceptable as an drop effect by default
+ * (since MS didn't make it part of the 'All' value)
+ * v2.2
+ * 2009-04-15 JPP - Separated DragSource.cs into DropSink.cs
+ * 2009-03-17 JPP - Initial version
+ *
+ * Copyright (C) 2009-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using System.Windows.Forms;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// An IDragSource controls how drag out from the ObjectListView will behave
+ ///
+ public interface IDragSource
+ {
+ ///
+ /// A drag operation is beginning. Return the data object that will be used
+ /// for data transfer. Return null to prevent the drag from starting. The data
+ /// object will normally include all the selected objects.
+ ///
+ ///
+ /// The returned object is later passed to the GetAllowedEffect() and EndDrag()
+ /// methods.
+ ///
+ /// What ObjectListView is being dragged from.
+ /// Which mouse button is down?
+ /// What item was directly dragged by the user? There may be more than just this
+ /// item selected.
+ /// The data object that will be used for data transfer. This will often be a subclass
+ /// of DataObject, but does not need to be.
+ Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item);
+
+ ///
+ /// What operations are possible for this drag? This controls the icon shown during the drag
+ ///
+ /// The data object returned by StartDrag()
+ /// A combination of DragDropEffects flags
+ DragDropEffects GetAllowedEffects(Object dragObject);
+
+ ///
+ /// The drag operation is complete. Do whatever is necessary to complete the action.
+ ///
+ /// The data object returned by StartDrag()
+ /// The value returned from GetAllowedEffects()
+ void EndDrag(Object dragObject, DragDropEffects effect);
+ }
+
+ ///
+ /// A do-nothing implementation of IDragSource that can be safely subclassed.
+ ///
+ public class AbstractDragSource : IDragSource
+ {
+ #region IDragSource Members
+
+ ///
+ /// See IDragSource documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) {
+ return null;
+ }
+
+ ///
+ /// See IDragSource documentation
+ ///
+ ///
+ ///
+ public virtual DragDropEffects GetAllowedEffects(Object data) {
+ return DragDropEffects.None;
+ }
+
+ ///
+ /// See IDragSource documentation
+ ///
+ ///
+ ///
+ public virtual void EndDrag(Object dragObject, DragDropEffects effect) {
+ }
+
+ #endregion
+ }
+
+ ///
+ /// A reasonable implementation of IDragSource that provides normal
+ /// drag source functionality. It creates a data object that supports
+ /// inter-application dragging of text and HTML representation of
+ /// the dragged rows. It can optionally force a refresh of all dragged
+ /// rows when the drag is complete.
+ ///
+ /// Subclasses can override GetDataObject() to add new
+ /// data formats to the data transfer object.
+ public class SimpleDragSource : IDragSource
+ {
+ #region Constructors
+
+ ///
+ /// Construct a SimpleDragSource
+ ///
+ public SimpleDragSource() {
+ }
+
+ ///
+ /// Construct a SimpleDragSource that refreshes the dragged rows when
+ /// the drag is complete
+ ///
+ ///
+ public SimpleDragSource(bool refreshAfterDrop) {
+ this.RefreshAfterDrop = refreshAfterDrop;
+ }
+
+ #endregion
+
+ #region Public properties
+
+ ///
+ /// Gets or sets whether the dragged rows should be refreshed when the
+ /// drag operation is complete.
+ ///
+ public bool RefreshAfterDrop {
+ get { return refreshAfterDrop; }
+ set { refreshAfterDrop = value; }
+ }
+ private bool refreshAfterDrop;
+
+ #endregion
+
+ #region IDragSource Members
+
+ ///
+ /// Create a DataObject when the user does a left mouse drag operation.
+ /// See IDragSource for further information.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) {
+ // We only drag on left mouse
+ if (button != MouseButtons.Left)
+ return null;
+
+ return this.CreateDataObject(olv);
+ }
+
+ ///
+ /// Which operations are allowed in the operation? By default, all operations are supported.
+ ///
+ ///
+ /// All operations are supported
+ public virtual DragDropEffects GetAllowedEffects(Object data) {
+ return DragDropEffects.All | DragDropEffects.Link; // why didn't MS include 'Link' in 'All'??
+ }
+
+ ///
+ /// The drag operation is finished. Refreshes the dragged rows if so configured.
+ ///
+ ///
+ ///
+ public virtual void EndDrag(Object dragObject, DragDropEffects effect) {
+ OLVDataObject data = dragObject as OLVDataObject;
+ if (data == null)
+ return;
+
+ if (this.RefreshAfterDrop)
+ data.ListView.RefreshObjects(data.ModelObjects);
+ }
+
+ ///
+ /// Create a data object that will be used to as the data object
+ /// for the drag operation.
+ ///
+ ///
+ /// Subclasses can override this method add new formats to the data object.
+ ///
+ /// The ObjectListView that is the source of the drag
+ /// A data object for the drag
+ protected virtual object CreateDataObject(ObjectListView olv) {
+ return new OLVDataObject(olv);
+ }
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/DragDrop/DropSink.cs b/ObjectListView/DragDrop/DropSink.cs
new file mode 100644
index 0000000..c82bd67
--- /dev/null
+++ b/ObjectListView/DragDrop/DropSink.cs
@@ -0,0 +1,1562 @@
+/*
+ * DropSink.cs - Add drop sink ability to an ObjectListView
+ *
+ * Author: Phillip Piper
+ * Date: 2009-03-17 5:15 PM
+ *
+ * Change log:
+ * 2018-04-26 JPP - Implemented LeftOfItem and RightOfItem target locations
+ * - Added support for rearranging on non-Detail views.
+ * v2.9
+ * 2015-07-08 JPP - Added SimpleDropSink.EnableFeedback to allow all the pretty and helpful
+ * user feedback during drags to be turned off
+ * v2.7
+ * 2011-04-20 JPP - Rewrote how ModelDropEventArgs.RefreshObjects() works on TreeListViews
+ * v2.4.1
+ * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource.
+ * v2.3
+ * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for
+ * objects that were children but are now roots.
+ * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after
+ * a drag-drop operation
+ * 2009-08-19 JPP - Changed to use OlvHitTest()
+ * v2.2.1
+ * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs
+ * v2.2
+ * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs
+ * - Tweaked the appearance of the drop-on-background feedback
+ * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs
+ * 2009-03-17 JPP - Initial version
+ *
+ * Copyright (C) 2009-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// Objects that implement this interface can acts as the receiver for drop
+ /// operation for an ObjectListView.
+ ///
+ public interface IDropSink
+ {
+ ///
+ /// Gets or sets the ObjectListView that is the drop sink
+ ///
+ ObjectListView ListView { get; set; }
+
+ ///
+ /// Draw any feedback that is appropriate to the current drop state.
+ ///
+ ///
+ /// Any drawing is done over the top of the ListView. This operation should disturb
+ /// the Graphic as little as possible. Specifically, do not erase the area into which
+ /// you draw.
+ ///
+ /// A Graphic for drawing
+ /// The contents bounds of the ListView (not including any header)
+ void DrawFeedback(Graphics g, Rectangle bounds);
+
+ ///
+ /// The user has released the drop over this control
+ ///
+ ///
+ /// Implementors should set args.Effect to the appropriate DragDropEffects. This value is returned
+ /// to the originator of the drag.
+ ///
+ ///
+ void Drop(DragEventArgs args);
+
+ ///
+ /// A drag has entered this control.
+ ///
+ /// Implementors should set args.Effect to the appropriate DragDropEffects.
+ ///
+ void Enter(DragEventArgs args);
+
+ ///
+ /// Change the cursor to reflect the current drag operation.
+ ///
+ ///
+ void GiveFeedback(GiveFeedbackEventArgs args);
+
+ ///
+ /// The drag has left the bounds of this control
+ ///
+ void Leave();
+
+ ///
+ /// The drag is moving over this control.
+ ///
+ /// This is where any drop target should be calculated.
+ /// Implementors should set args.Effect to the appropriate DragDropEffects.
+ ///
+ ///
+ void Over(DragEventArgs args);
+
+ ///
+ /// Should the drag be allowed to continue?
+ ///
+ ///
+ void QueryContinue(QueryContinueDragEventArgs args);
+ }
+
+ ///
+ /// This is a do-nothing implementation of IDropSink that is a useful
+ /// base class for more sophisticated implementations.
+ ///
+ public class AbstractDropSink : IDropSink
+ {
+ #region IDropSink Members
+
+ ///
+ /// Gets or sets the ObjectListView that is the drop sink
+ ///
+ public virtual ObjectListView ListView {
+ get { return listView; }
+ set { this.listView = value; }
+ }
+ private ObjectListView listView;
+
+ ///
+ /// Draw any feedback that is appropriate to the current drop state.
+ ///
+ ///
+ /// Any drawing is done over the top of the ListView. This operation should disturb
+ /// the Graphic as little as possible. Specifically, do not erase the area into which
+ /// you draw.
+ ///
+ /// A Graphic for drawing
+ /// The contents bounds of the ListView (not including any header)
+ public virtual void DrawFeedback(Graphics g, Rectangle bounds) {
+ }
+
+ ///
+ /// The user has released the drop over this control
+ ///
+ ///
+ /// Implementors should set args.Effect to the appropriate DragDropEffects. This value is returned
+ /// to the originator of the drag.
+ ///
+ ///
+ public virtual void Drop(DragEventArgs args) {
+ this.Cleanup();
+ }
+
+ ///
+ /// A drag has entered this control.
+ ///
+ /// Implementors should set args.Effect to the appropriate DragDropEffects.
+ ///
+ public virtual void Enter(DragEventArgs args) {
+ }
+
+ ///
+ /// The drag has left the bounds of this control
+ ///
+ public virtual void Leave() {
+ this.Cleanup();
+ }
+
+ ///
+ /// The drag is moving over this control.
+ ///
+ /// This is where any drop target should be calculated.
+ /// Implementors should set args.Effect to the appropriate DragDropEffects.
+ ///
+ ///
+ public virtual void Over(DragEventArgs args) {
+ }
+
+ ///
+ /// Change the cursor to reflect the current drag operation.
+ ///
+ /// You only need to override this if you want non-standard cursors.
+ /// The standard cursors are supplied automatically.
+ ///
+ public virtual void GiveFeedback(GiveFeedbackEventArgs args) {
+ args.UseDefaultCursors = true;
+ }
+
+ ///
+ /// Should the drag be allowed to continue?
+ ///
+ ///
+ /// You only need to override this if you want the user to be able
+ /// to end the drop in some non-standard way, e.g. dragging to a
+ /// certain point even without releasing the mouse, or going outside
+ /// the bounds of the application.
+ ///
+ ///
+ public virtual void QueryContinue(QueryContinueDragEventArgs args) {
+ }
+
+
+ #endregion
+
+ #region Commands
+
+ ///
+ /// This is called when the mouse leaves the drop region and after the
+ /// drop has completed.
+ ///
+ protected virtual void Cleanup() {
+ }
+
+ #endregion
+ }
+
+ ///
+ /// The enum indicates which target has been found for a drop operation
+ ///
+ [Flags]
+ public enum DropTargetLocation
+ {
+ ///
+ /// No applicable target has been found
+ ///
+ None = 0,
+
+ ///
+ /// The list itself is the target of the drop
+ ///
+ Background = 0x01,
+
+ ///
+ /// An item is the target
+ ///
+ Item = 0x02,
+
+ ///
+ /// Between two items (or above the top item or below the bottom item)
+ /// can be the target. This is not actually ever a target, only a value indicate
+ /// that it is valid to drop between items
+ ///
+ BetweenItems = 0x04,
+
+ ///
+ /// Above an item is the target
+ ///
+ AboveItem = 0x08,
+
+ ///
+ /// Below an item is the target
+ ///
+ BelowItem = 0x10,
+
+ ///
+ /// A subitem is the target of the drop
+ ///
+ SubItem = 0x20,
+
+ ///
+ /// On the right of an item is the target
+ ///
+ RightOfItem = 0x40,
+
+ ///
+ /// On the left of an item is the target
+ ///
+ LeftOfItem = 0x80
+ }
+
+ ///
+ /// This class represents a simple implementation of a drop sink.
+ ///
+ ///
+ /// Actually, it should be called CleverDropSink -- it's far from simple and can do quite a lot in its own right.
+ ///
+ public class SimpleDropSink : AbstractDropSink
+ {
+ #region Life and death
+
+ ///
+ /// Make a new drop sink
+ ///
+ public SimpleDropSink() {
+ this.timer = new Timer();
+ this.timer.Interval = 250;
+ this.timer.Tick += new EventHandler(this.timer_Tick);
+
+ this.CanDropOnItem = true;
+ //this.CanDropOnSubItem = true;
+ //this.CanDropOnBackground = true;
+ //this.CanDropBetween = true;
+
+ this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue);
+ this.billboard = new BillboardOverlay();
+ }
+
+ #endregion
+
+ #region Public properties
+
+ ///
+ /// Get or set the locations where a drop is allowed to occur (OR-ed together)
+ ///
+ public DropTargetLocation AcceptableLocations {
+ get { return this.acceptableLocations; }
+ set { this.acceptableLocations = value; }
+ }
+ private DropTargetLocation acceptableLocations;
+
+ ///
+ /// Gets or sets whether this sink allows model objects to be dragged from other lists. Defaults to true.
+ ///
+ public bool AcceptExternal {
+ get { return this.acceptExternal; }
+ set { this.acceptExternal = value; }
+ }
+ private bool acceptExternal = true;
+
+ ///
+ /// Gets or sets whether the ObjectListView should scroll when the user drags
+ /// something near to the top or bottom rows. Defaults to true.
+ ///
+ /// AutoScroll does not scroll horizontally.
+ public bool AutoScroll {
+ get { return this.autoScroll; }
+ set { this.autoScroll = value; }
+ }
+ private bool autoScroll = true;
+
+ ///
+ /// Gets the billboard overlay that will be used to display feedback
+ /// messages during a drag operation.
+ ///
+ /// Set this to null to stop the feedback.
+ public BillboardOverlay Billboard {
+ get { return this.billboard; }
+ set { this.billboard = value; }
+ }
+ private BillboardOverlay billboard;
+
+ ///
+ /// Get or set whether a drop can occur between items of the list
+ ///
+ public bool CanDropBetween {
+ get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; }
+ set {
+ if (value)
+ this.AcceptableLocations |= DropTargetLocation.BetweenItems;
+ else
+ this.AcceptableLocations &= ~DropTargetLocation.BetweenItems;
+ }
+ }
+
+ ///
+ /// Get or set whether a drop can occur on the listview itself
+ ///
+ public bool CanDropOnBackground {
+ get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; }
+ set {
+ if (value)
+ this.AcceptableLocations |= DropTargetLocation.Background;
+ else
+ this.AcceptableLocations &= ~DropTargetLocation.Background;
+ }
+ }
+
+ ///
+ /// Get or set whether a drop can occur on items in the list
+ ///
+ public bool CanDropOnItem {
+ get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; }
+ set {
+ if (value)
+ this.AcceptableLocations |= DropTargetLocation.Item;
+ else
+ this.AcceptableLocations &= ~DropTargetLocation.Item;
+ }
+ }
+
+ ///
+ /// Get or set whether a drop can occur on a subitem in the list
+ ///
+ public bool CanDropOnSubItem {
+ get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; }
+ set {
+ if (value)
+ this.AcceptableLocations |= DropTargetLocation.SubItem;
+ else
+ this.AcceptableLocations &= ~DropTargetLocation.SubItem;
+ }
+ }
+
+ ///
+ /// Gets or sets whether the drop sink should draw feedback onto the given list
+ /// during the drag operation. Defaults to true.
+ ///
+ /// If this is false, you will have to give the user feedback in some
+ /// other fashion, like cursor changes
+ public bool EnableFeedback {
+ get { return enableFeedback; }
+ set { enableFeedback = value; }
+ }
+ private bool enableFeedback = true;
+
+ ///
+ /// Get or set the index of the item that is the target of the drop
+ ///
+ public int DropTargetIndex {
+ get { return dropTargetIndex; }
+ set {
+ if (this.dropTargetIndex != value) {
+ this.dropTargetIndex = value;
+ this.ListView.Invalidate();
+ }
+ }
+ }
+ private int dropTargetIndex = -1;
+
+ ///
+ /// Get the item that is the target of the drop
+ ///
+ public OLVListItem DropTargetItem {
+ get {
+ return this.ListView.GetItem(this.DropTargetIndex);
+ }
+ }
+
+ ///
+ /// Get or set the location of the target of the drop
+ ///
+ public DropTargetLocation DropTargetLocation {
+ get { return dropTargetLocation; }
+ set {
+ if (this.dropTargetLocation != value) {
+ this.dropTargetLocation = value;
+ this.ListView.Invalidate();
+ }
+ }
+ }
+ private DropTargetLocation dropTargetLocation;
+
+ ///
+ /// Get or set the index of the subitem that is the target of the drop
+ ///
+ public int DropTargetSubItemIndex {
+ get { return dropTargetSubItemIndex; }
+ set {
+ if (this.dropTargetSubItemIndex != value) {
+ this.dropTargetSubItemIndex = value;
+ this.ListView.Invalidate();
+ }
+ }
+ }
+ private int dropTargetSubItemIndex = -1;
+
+ ///
+ /// Get or set the color that will be used to provide drop feedback
+ ///
+ public Color FeedbackColor {
+ get { return this.feedbackColor; }
+ set { this.feedbackColor = value; }
+ }
+ private Color feedbackColor;
+
+ ///
+ /// Get whether the alt key was down during this drop event
+ ///
+ public bool IsAltDown {
+ get { return (this.KeyState & 32) == 32; }
+ }
+
+ ///
+ /// Get whether any modifier key was down during this drop event
+ ///
+ public bool IsAnyModifierDown {
+ get { return (this.KeyState & (4 + 8 + 32)) != 0; }
+ }
+
+ ///
+ /// Get whether the control key was down during this drop event
+ ///
+ public bool IsControlDown {
+ get { return (this.KeyState & 8) == 8; }
+ }
+
+ ///
+ /// Get whether the left mouse button was down during this drop event
+ ///
+ public bool IsLeftMouseButtonDown {
+ get { return (this.KeyState & 1) == 1; }
+ }
+
+ ///
+ /// Get whether the right mouse button was down during this drop event
+ ///
+ public bool IsMiddleMouseButtonDown {
+ get { return (this.KeyState & 16) == 16; }
+ }
+
+ ///
+ /// Get whether the right mouse button was down during this drop event
+ ///
+ public bool IsRightMouseButtonDown {
+ get { return (this.KeyState & 2) == 2; }
+ }
+
+ ///
+ /// Get whether the shift key was down during this drop event
+ ///
+ public bool IsShiftDown {
+ get { return (this.KeyState & 4) == 4; }
+ }
+
+ ///
+ /// Get or set the state of the keys during this drop event
+ ///
+ public int KeyState {
+ get { return this.keyState; }
+ set { this.keyState = value; }
+ }
+ private int keyState;
+
+ ///
+ /// Gets or sets whether the drop sink will automatically use cursors
+ /// based on the drop effect. By default, this is true. If this is
+ /// set to false, you must set the Cursor yourself.
+ ///
+ public bool UseDefaultCursors {
+ get { return useDefaultCursors; }
+ set { useDefaultCursors = value; }
+ }
+ private bool useDefaultCursors = true;
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// Triggered when the sink needs to know if a drop can occur.
+ ///
+ ///
+ /// Handlers should set Effect to indicate what is possible.
+ /// Handlers can change any of the DropTarget* settings to change
+ /// the target of the drop.
+ ///
+ public event EventHandler CanDrop;
+
+ ///
+ /// Triggered when the drop is made.
+ ///
+ public event EventHandler Dropped;
+
+ ///
+ /// Triggered when the sink needs to know if a drop can occur
+ /// AND the source is an ObjectListView
+ ///
+ ///
+ /// Handlers should set Effect to indicate what is possible.
+ /// Handlers can change any of the DropTarget* settings to change
+ /// the target of the drop.
+ ///
+ public event EventHandler ModelCanDrop;
+
+ ///
+ /// Triggered when the drop is made.
+ /// AND the source is an ObjectListView
+ ///
+ public event EventHandler ModelDropped;
+
+ #endregion
+
+ #region DropSink Interface
+
+ ///
+ /// Cleanup the drop sink when the mouse has left the control or
+ /// the drag has finished.
+ ///
+ protected override void Cleanup() {
+ this.DropTargetLocation = DropTargetLocation.None;
+ this.ListView.FullRowSelect = this.originalFullRowSelect;
+ this.Billboard.Text = null;
+ }
+
+ ///
+ /// Draw any feedback that is appropriate to the current drop state.
+ ///
+ ///
+ /// Any drawing is done over the top of the ListView. This operation should disturb
+ /// the Graphic as little as possible. Specifically, do not erase the area into which
+ /// you draw.
+ ///
+ /// A Graphic for drawing
+ /// The contents bounds of the ListView (not including any header)
+ public override void DrawFeedback(Graphics g, Rectangle bounds) {
+ g.SmoothingMode = ObjectListView.SmoothingMode;
+
+ if (this.EnableFeedback) {
+ switch (this.DropTargetLocation) {
+ case DropTargetLocation.Background:
+ this.DrawFeedbackBackgroundTarget(g, bounds);
+ break;
+ case DropTargetLocation.Item:
+ this.DrawFeedbackItemTarget(g, bounds);
+ break;
+ case DropTargetLocation.AboveItem:
+ this.DrawFeedbackAboveItemTarget(g, bounds);
+ break;
+ case DropTargetLocation.BelowItem:
+ this.DrawFeedbackBelowItemTarget(g, bounds);
+ break;
+ case DropTargetLocation.LeftOfItem:
+ this.DrawFeedbackLeftOfItemTarget(g, bounds);
+ break;
+ case DropTargetLocation.RightOfItem:
+ this.DrawFeedbackRightOfItemTarget(g, bounds);
+ break;
+ }
+ }
+
+ if (this.Billboard != null) {
+ this.Billboard.Draw(this.ListView, g, bounds);
+ }
+ }
+
+ ///
+ /// The user has released the drop over this control
+ ///
+ ///
+ public override void Drop(DragEventArgs args) {
+ this.dropEventArgs.DragEventArgs = args;
+ this.TriggerDroppedEvent(args);
+ this.timer.Stop();
+ this.Cleanup();
+ }
+
+ ///
+ /// A drag has entered this control.
+ ///
+ /// Implementors should set args.Effect to the appropriate DragDropEffects.
+ ///
+ public override void Enter(DragEventArgs args) {
+ //System.Diagnostics.Debug.WriteLine("Enter");
+
+ /*
+ * When FullRowSelect is true, we have two problems:
+ * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes
+ * up our calculation of the drop rectangle.
+ * 2) during the drag, the Timer events will not fire! This is the major problem, since without
+ * those events we can't autoscroll.
+ *
+ * The first problem we can solve through coding, but the second is more difficult.
+ * We avoid both problems by turning off FullRowSelect during the drop operation.
+ */
+ this.originalFullRowSelect = this.ListView.FullRowSelect;
+ this.ListView.FullRowSelect = false;
+
+ // Setup our drop event args block
+ this.dropEventArgs = new ModelDropEventArgs();
+ this.dropEventArgs.DropSink = this;
+ this.dropEventArgs.ListView = this.ListView;
+ this.dropEventArgs.DragEventArgs = args;
+ this.dropEventArgs.DataObject = args.Data;
+ OLVDataObject olvData = args.Data as OLVDataObject;
+ if (olvData != null) {
+ this.dropEventArgs.SourceListView = olvData.ListView;
+ this.dropEventArgs.SourceModels = olvData.ModelObjects;
+ }
+
+ this.Over(args);
+ }
+
+ ///
+ /// Change the cursor to reflect the current drag operation.
+ ///
+ ///
+ public override void GiveFeedback(GiveFeedbackEventArgs args) {
+ args.UseDefaultCursors = this.UseDefaultCursors;
+ }
+
+ ///
+ /// The drag is moving over this control.
+ ///
+ ///
+ public override void Over(DragEventArgs args) {
+ //System.Diagnostics.Debug.WriteLine("Over");
+ this.dropEventArgs.DragEventArgs = args;
+ this.KeyState = args.KeyState;
+ Point pt = this.ListView.PointToClient(new Point(args.X, args.Y));
+ args.Effect = this.CalculateDropAction(args, pt);
+ this.CheckScrolling(pt);
+ }
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// Trigger the Dropped events
+ ///
+ ///
+ protected virtual void TriggerDroppedEvent(DragEventArgs args) {
+ this.dropEventArgs.Handled = false;
+
+ // If the source is an ObjectListView, trigger the ModelDropped event
+ if (this.dropEventArgs.SourceListView != null)
+ this.OnModelDropped(this.dropEventArgs);
+
+ if (!this.dropEventArgs.Handled)
+ this.OnDropped(this.dropEventArgs);
+ }
+
+ ///
+ /// Trigger CanDrop
+ ///
+ ///
+ protected virtual void OnCanDrop(OlvDropEventArgs args) {
+ if (this.CanDrop != null)
+ this.CanDrop(this, args);
+ }
+
+ ///
+ /// Trigger Dropped
+ ///
+ ///
+ protected virtual void OnDropped(OlvDropEventArgs args) {
+ if (this.Dropped != null)
+ this.Dropped(this, args);
+ }
+
+ ///
+ /// Trigger ModelCanDrop
+ ///
+ ///
+ protected virtual void OnModelCanDrop(ModelDropEventArgs args) {
+
+ // Don't allow drops from other list, if that's what's configured
+ if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) {
+ args.Effect = DragDropEffects.None;
+ args.DropTargetLocation = DropTargetLocation.None;
+ args.InfoMessage = "This list doesn't accept drops from other lists";
+ return;
+ }
+
+ if (this.ModelCanDrop != null)
+ this.ModelCanDrop(this, args);
+ }
+
+ ///
+ /// Trigger ModelDropped
+ ///
+ ///
+ protected virtual void OnModelDropped(ModelDropEventArgs args) {
+ if (this.ModelDropped != null)
+ this.ModelDropped(this, args);
+ }
+
+ #endregion
+
+ #region Implementation
+
+ private void timer_Tick(object sender, EventArgs e) {
+ this.HandleTimerTick();
+ }
+
+ ///
+ /// Handle the timer tick event, which is sent when the listview should
+ /// scroll
+ ///
+ protected virtual void HandleTimerTick() {
+
+ // If the mouse has been released, stop scrolling.
+ // This is only necessary if the mouse is released outside of the control.
+ // If the mouse is released inside the control, we would receive a Drop event.
+ if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) ||
+ (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) ||
+ (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) {
+ this.timer.Stop();
+ this.Cleanup();
+ return;
+ }
+
+ // Auto scrolling will continue while the mouse is close to the ListView
+ const int GRACE_PERIMETER = 30;
+
+ Point pt = this.ListView.PointToClient(Cursor.Position);
+ Rectangle r2 = this.ListView.ClientRectangle;
+ r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER);
+ if (r2.Contains(pt)) {
+ this.ListView.LowLevelScroll(0, this.scrollAmount);
+ }
+ }
+
+ ///
+ /// When the mouse is at the given point, what should the target of the drop be?
+ ///
+ /// This method should update the DropTarget* members of the given arg block
+ ///
+ /// The mouse point, in client co-ordinates
+ protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) {
+ const int SMALL_VALUE = 3;
+ DropTargetLocation location = DropTargetLocation.None;
+ int targetIndex = -1;
+ int targetSubIndex = 0;
+
+ if (this.CanDropOnBackground)
+ location = DropTargetLocation.Background;
+
+ // Which item is the mouse over?
+ // If it is not over any item, it's over the background.
+ OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y);
+ if (info.Item != null && this.CanDropOnItem) {
+ location = DropTargetLocation.Item;
+ targetIndex = info.Item.Index;
+ if (info.SubItem != null && this.CanDropOnSubItem)
+ targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem);
+ }
+
+ // Check to see if the mouse is "between" rows.
+ // ("between" is somewhat loosely defined).
+ // If the view is Details or List, then "between" is considered vertically.
+ // If the view is SmallIcon, LargeIcon or Tile, then "between" is considered horizontally.
+ if (this.CanDropBetween && this.ListView.GetItemCount() > 0) {
+
+ switch (this.ListView.View) {
+ case View.LargeIcon:
+ case View.Tile:
+ case View.SmallIcon:
+ // If the mouse is over an item, check to see if it is near the left or right edge.
+ if (info.Item != null) {
+ int delta = this.CanDropOnItem ? SMALL_VALUE : info.Item.Bounds.Width / 2;
+ if (pt.X <= info.Item.Bounds.Left + delta) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.LeftOfItem;
+ } else if (pt.X >= info.Item.Bounds.Right - delta) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.RightOfItem;
+ }
+ } else {
+ // Is there an item a little to the *right* of the mouse?
+ // If so, we say the drop point is *left* that item
+ int probeWidth = SMALL_VALUE * 2;
+ info = this.ListView.OlvHitTest(pt.X + probeWidth, pt.Y);
+ if (info.Item != null) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.LeftOfItem;
+ } else {
+ // Is there an item a little to the left of the mouse?
+ info = this.ListView.OlvHitTest(pt.X - probeWidth, pt.Y);
+ if (info.Item != null) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.RightOfItem;
+ }
+ }
+ }
+ break;
+ case View.Details:
+ case View.List:
+ // If the mouse is over an item, check to see if it is near the top or bottom
+ if (info.Item != null) {
+ int delta = this.CanDropOnItem ? SMALL_VALUE : this.ListView.RowHeightEffective / 2;
+
+ if (pt.Y <= info.Item.Bounds.Top + delta) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.AboveItem;
+ } else if (pt.Y >= info.Item.Bounds.Bottom - delta) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.BelowItem;
+ }
+ } else {
+ // Is there an item a little below the mouse?
+ // If so, we say the drop point is above that row
+ info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE);
+ if (info.Item != null) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.AboveItem;
+ } else {
+ // Is there an item a little above the mouse?
+ info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE);
+ if (info.Item != null) {
+ targetIndex = info.Item.Index;
+ location = DropTargetLocation.BelowItem;
+ }
+ }
+ }
+
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ args.DropTargetLocation = location;
+ args.DropTargetIndex = targetIndex;
+ args.DropTargetSubItemIndex = targetSubIndex;
+ }
+
+ ///
+ /// What sort of action is possible when the mouse is at the given point?
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) {
+
+ this.CalculateDropTarget(this.dropEventArgs, pt);
+
+ this.dropEventArgs.MouseLocation = pt;
+ this.dropEventArgs.InfoMessage = null;
+ this.dropEventArgs.Handled = false;
+
+ if (this.dropEventArgs.SourceListView != null) {
+ this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex);
+ this.OnModelCanDrop(this.dropEventArgs);
+ }
+
+ if (!this.dropEventArgs.Handled)
+ this.OnCanDrop(this.dropEventArgs);
+
+ this.UpdateAfterCanDropEvent(this.dropEventArgs);
+
+ return this.dropEventArgs.Effect;
+ }
+
+ ///
+ /// Based solely on the state of the modifier keys, what drop operation should
+ /// be used?
+ ///
+ /// The drop operation that matches the state of the keys
+ public DragDropEffects CalculateStandardDropActionFromKeys() {
+ if (this.IsControlDown) {
+ if (this.IsShiftDown)
+ return DragDropEffects.Link;
+ else
+ return DragDropEffects.Copy;
+ } else {
+ return DragDropEffects.Move;
+ }
+ }
+
+ ///
+ /// Should the listview be made to scroll when the mouse is at the given point?
+ ///
+ ///
+ protected virtual void CheckScrolling(Point pt) {
+ if (!this.AutoScroll)
+ return;
+
+ Rectangle r = this.ListView.ContentRectangle;
+ int rowHeight = this.ListView.RowHeightEffective;
+ int close = rowHeight;
+
+ // In Tile view, using the whole row height is too much
+ if (this.ListView.View == View.Tile)
+ close /= 2;
+
+ if (pt.Y <= (r.Top + close)) {
+ // Scroll faster if the mouse is closer to the top
+ this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350);
+ this.timer.Start();
+ this.scrollAmount = -rowHeight;
+ } else {
+ if (pt.Y >= (r.Bottom - close)) {
+ this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350);
+ this.timer.Start();
+ this.scrollAmount = rowHeight;
+ } else {
+ this.timer.Stop();
+ }
+ }
+ }
+
+ ///
+ /// Update the state of our sink to reflect the information that
+ /// may have been written into the drop event args.
+ ///
+ ///
+ protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) {
+ this.DropTargetIndex = args.DropTargetIndex;
+ this.DropTargetLocation = args.DropTargetLocation;
+ this.DropTargetSubItemIndex = args.DropTargetSubItemIndex;
+
+ if (this.Billboard != null) {
+ Point pt = args.MouseLocation;
+ pt.Offset(5, 5);
+ if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) {
+ this.Billboard.Text = this.dropEventArgs.InfoMessage;
+ this.Billboard.Location = pt;
+ this.ListView.Invalidate();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Rendering
+
+ ///
+ /// Draw the feedback that shows that the background is the target
+ ///
+ ///
+ ///
+ protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) {
+ float penWidth = 12.0f;
+ Rectangle r = bounds;
+ r.Inflate((int)-penWidth / 2, (int)-penWidth / 2);
+ using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) {
+ using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) {
+ g.DrawPath(p, path);
+ }
+ }
+ }
+
+ ///
+ /// Draw the feedback that shows that an item (or a subitem) is the target
+ ///
+ ///
+ ///
+ ///
+ /// DropTargetItem and DropTargetSubItemIndex tells what is the target
+ ///
+ protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) {
+ if (this.DropTargetItem == null)
+ return;
+ Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex);
+ r.Inflate(1, 1);
+ float diameter = r.Height / 3;
+ using (GraphicsPath path = this.GetRoundedRect(r, diameter)) {
+ using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) {
+ g.FillPath(b, path);
+ }
+ using (Pen p = new Pen(this.FeedbackColor, 3.0f)) {
+ g.DrawPath(p, path);
+ }
+ }
+ }
+
+ ///
+ /// Draw the feedback that shows the drop will occur before target
+ ///
+ ///
+ ///
+ protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) {
+ if (this.DropTargetItem == null)
+ return;
+
+ Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex);
+ this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top);
+ }
+
+ ///
+ /// Draw the feedback that shows the drop will occur after target
+ ///
+ ///
+ ///
+ protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds)
+ {
+ if (this.DropTargetItem == null)
+ return;
+
+ Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex);
+ this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom);
+ }
+
+ ///
+ /// Draw the feedback that shows the drop will occur to the left of target
+ ///
+ ///
+ ///
+ protected virtual void DrawFeedbackLeftOfItemTarget(Graphics g, Rectangle bounds)
+ {
+ if (this.DropTargetItem == null)
+ return;
+
+ Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex);
+ this.DrawBetweenLine(g, r.Left, r.Top, r.Left, r.Bottom);
+ }
+
+ ///
+ /// Draw the feedback that shows the drop will occur to the right of target
+ ///
+ ///
+ ///
+ protected virtual void DrawFeedbackRightOfItemTarget(Graphics g, Rectangle bounds)
+ {
+ if (this.DropTargetItem == null)
+ return;
+
+ Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex);
+ this.DrawBetweenLine(g, r.Right, r.Top, r.Right, r.Bottom);
+ }
+
+ ///
+ /// Return a GraphicPath that is round corner rectangle.
+ ///
+ ///
+ ///
+ ///
+ protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) {
+ GraphicsPath path = new GraphicsPath();
+
+ RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter);
+ path.AddArc(arc, 180, 90);
+ arc.X = rect.Right - diameter;
+ path.AddArc(arc, 270, 90);
+ arc.Y = rect.Bottom - diameter;
+ path.AddArc(arc, 0, 90);
+ arc.X = rect.Left;
+ path.AddArc(arc, 90, 90);
+ path.CloseFigure();
+
+ return path;
+ }
+
+ ///
+ /// Calculate the target rectangle when the given item (and possible subitem)
+ /// is the target of the drop.
+ ///
+ ///
+ ///
+ ///
+ protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) {
+ if (subItem > 0)
+ return item.SubItems[subItem].Bounds;
+
+ Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem);
+
+ // Allow for indent
+ if (item.IndentCount > 0) {
+ int indentWidth = this.ListView.SmallImageSize.Width * item.IndentCount;
+ r.X += indentWidth;
+ r.Width -= indentWidth;
+ }
+
+ return r;
+ }
+
+ ///
+ /// Draw a "between items" line at the given co-ordinates
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) {
+ using (Brush b = new SolidBrush(this.FeedbackColor)) {
+ if (y1 == y2) {
+ // Put right and left arrow on a horizontal line
+ DrawClosedFigure(g, b, RightPointingArrow(x1, y1));
+ DrawClosedFigure(g, b, LeftPointingArrow(x2, y2));
+ } else {
+ // Put up and down arrows on a vertical line
+ DrawClosedFigure(g, b, DownPointingArrow(x1, y1));
+ DrawClosedFigure(g, b, UpPointingArrow(x2, y2));
+ }
+ }
+
+ using (Pen p = new Pen(this.FeedbackColor, 3.0f)) {
+ g.DrawLine(p, x1, y1, x2, y2);
+ }
+ }
+
+ private const int ARROW_SIZE = 6;
+
+ private static void DrawClosedFigure(Graphics g, Brush b, Point[] pts) {
+ using (GraphicsPath gp = new GraphicsPath()) {
+ gp.StartFigure();
+ gp.AddLines(pts);
+ gp.CloseFigure();
+ g.FillPath(b, gp);
+ }
+ }
+
+ private static Point[] RightPointingArrow(int x, int y) {
+ return new Point[] {
+ new Point(x, y - ARROW_SIZE),
+ new Point(x, y + ARROW_SIZE),
+ new Point(x + ARROW_SIZE, y)
+ };
+ }
+
+ private static Point[] LeftPointingArrow(int x, int y) {
+ return new Point[] {
+ new Point(x, y - ARROW_SIZE),
+ new Point(x, y + ARROW_SIZE),
+ new Point(x - ARROW_SIZE, y)
+ };
+ }
+
+ private static Point[] DownPointingArrow(int x, int y) {
+ return new Point[] {
+ new Point(x - ARROW_SIZE, y),
+ new Point(x + ARROW_SIZE, y),
+ new Point(x, y + ARROW_SIZE)
+ };
+ }
+
+ private static Point[] UpPointingArrow(int x, int y) {
+ return new Point[] {
+ new Point(x - ARROW_SIZE, y),
+ new Point(x + ARROW_SIZE, y),
+ new Point(x, y - ARROW_SIZE)
+ };
+ }
+
+ #endregion
+
+ private Timer timer;
+ private int scrollAmount;
+ private bool originalFullRowSelect;
+ private ModelDropEventArgs dropEventArgs;
+ }
+
+ ///
+ /// This drop sink allows items within the same list to be rearranged,
+ /// as well as allowing items to be dropped from other lists.
+ ///
+ ///
+ ///
+ /// This class can only be used on plain ObjectListViews and FastObjectListViews.
+ /// The other flavours have no way to implement the insert operation that is required.
+ ///
+ ///
+ /// This class does not work with grouping.
+ ///
+ ///
+ /// This class works when the OLV is sorted, but it is up to the programmer
+ /// to decide what rearranging such lists "means". Example: if the control is sorting
+ /// students by academic grade, and the user drags a "Fail" grade student up amongst the "A+"
+ /// students, it is the responsibility of the programmer to makes the appropriate changes
+ /// to the model and redraw/rebuild the control so that the users action makes sense.
+ ///
+ ///
+ /// Users of this class should listen for the CanDrop event to decide
+ /// if models from another OLV can be moved to OLV under this sink.
+ ///
+ ///
+ public class RearrangingDropSink : SimpleDropSink
+ {
+ ///
+ /// Create a RearrangingDropSink
+ ///
+ public RearrangingDropSink() {
+ this.CanDropBetween = true;
+ this.CanDropOnBackground = true;
+ this.CanDropOnItem = false;
+ }
+
+ ///
+ /// Create a RearrangingDropSink
+ ///
+ ///
+ public RearrangingDropSink(bool acceptDropsFromOtherLists)
+ : this() {
+ this.AcceptExternal = acceptDropsFromOtherLists;
+ }
+
+ ///
+ /// Trigger OnModelCanDrop
+ ///
+ ///
+ protected override void OnModelCanDrop(ModelDropEventArgs args) {
+ base.OnModelCanDrop(args);
+
+ if (args.Handled)
+ return;
+
+ args.Effect = DragDropEffects.Move;
+
+ // Don't allow drops from other list, if that's what's configured
+ if (!this.AcceptExternal && args.SourceListView != this.ListView) {
+ args.Effect = DragDropEffects.None;
+ args.DropTargetLocation = DropTargetLocation.None;
+ args.InfoMessage = "This list doesn't accept drops from other lists";
+ }
+
+ // If we are rearranging the same list, don't allow drops on the background
+ if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) {
+ args.Effect = DragDropEffects.None;
+ args.DropTargetLocation = DropTargetLocation.None;
+ }
+ }
+
+ ///
+ /// Trigger OnModelDropped
+ ///
+ ///
+ protected override void OnModelDropped(ModelDropEventArgs args) {
+ base.OnModelDropped(args);
+
+ if (!args.Handled)
+ this.RearrangeModels(args);
+ }
+
+ ///
+ /// Do the work of processing the dropped items
+ ///
+ ///
+ public virtual void RearrangeModels(ModelDropEventArgs args) {
+ switch (args.DropTargetLocation) {
+ case DropTargetLocation.AboveItem:
+ case DropTargetLocation.LeftOfItem:
+ this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels);
+ break;
+ case DropTargetLocation.BelowItem:
+ case DropTargetLocation.RightOfItem:
+ this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels);
+ break;
+ case DropTargetLocation.Background:
+ this.ListView.AddObjects(args.SourceModels);
+ break;
+ default:
+ return;
+ }
+
+ if (args.SourceListView != this.ListView) {
+ args.SourceListView.RemoveObjects(args.SourceModels);
+ }
+
+ // Some views have to be "encouraged" to show the changes
+ switch (this.ListView.View) {
+ case View.LargeIcon:
+ case View.SmallIcon:
+ case View.Tile:
+ this.ListView.BuildList();
+ break;
+ }
+ }
+ }
+
+ ///
+ /// When a drop sink needs to know if something can be dropped, or
+ /// to notify that a drop has occurred, it uses an instance of this class.
+ ///
+ public class OlvDropEventArgs : EventArgs
+ {
+ ///
+ /// Create a OlvDropEventArgs
+ ///
+ public OlvDropEventArgs() {
+ }
+
+ #region Data Properties
+
+ ///
+ /// Get the original drag-drop event args
+ ///
+ public DragEventArgs DragEventArgs
+ {
+ get { return this.dragEventArgs; }
+ internal set { this.dragEventArgs = value; }
+ }
+ private DragEventArgs dragEventArgs;
+
+ ///
+ /// Get the data object that is being dragged
+ ///
+ public object DataObject
+ {
+ get { return this.dataObject; }
+ internal set { this.dataObject = value; }
+ }
+ private object dataObject;
+
+ ///
+ /// Get the drop sink that originated this event
+ ///
+ public SimpleDropSink DropSink {
+ get { return this.dropSink; }
+ internal set { this.dropSink = value; }
+ }
+ private SimpleDropSink dropSink;
+
+ ///
+ /// Get or set the index of the item that is the target of the drop
+ ///
+ public int DropTargetIndex {
+ get { return dropTargetIndex; }
+ set { this.dropTargetIndex = value; }
+ }
+ private int dropTargetIndex = -1;
+
+ ///
+ /// Get or set the location of the target of the drop
+ ///
+ public DropTargetLocation DropTargetLocation {
+ get { return dropTargetLocation; }
+ set { this.dropTargetLocation = value; }
+ }
+ private DropTargetLocation dropTargetLocation;
+
+ ///
+ /// Get or set the index of the subitem that is the target of the drop
+ ///
+ public int DropTargetSubItemIndex {
+ get { return dropTargetSubItemIndex; }
+ set { this.dropTargetSubItemIndex = value; }
+ }
+ private int dropTargetSubItemIndex = -1;
+
+ ///
+ /// Get the item that is the target of the drop
+ ///
+ public OLVListItem DropTargetItem {
+ get {
+ return this.ListView.GetItem(this.DropTargetIndex);
+ }
+ set {
+ if (value == null)
+ this.DropTargetIndex = -1;
+ else
+ this.DropTargetIndex = value.Index;
+ }
+ }
+
+ ///
+ /// Get or set the drag effect that should be used for this operation
+ ///
+ public DragDropEffects Effect {
+ get { return this.effect; }
+ set { this.effect = value; }
+ }
+ private DragDropEffects effect;
+
+ ///
+ /// Get or set if this event was handled. No further processing will be done for a handled event.
+ ///
+ public bool Handled {
+ get { return this.handled; }
+ set { this.handled = value; }
+ }
+ private bool handled;
+
+ ///
+ /// Get or set the feedback message for this operation
+ ///
+ ///
+ /// If this is not null, it will be displayed as a feedback message
+ /// during the drag.
+ ///
+ public string InfoMessage {
+ get { return this.infoMessage; }
+ set { this.infoMessage = value; }
+ }
+ private string infoMessage;
+
+ ///
+ /// Get the ObjectListView that is being dropped on
+ ///
+ public ObjectListView ListView {
+ get { return this.listView; }
+ internal set { this.listView = value; }
+ }
+ private ObjectListView listView;
+
+ ///
+ /// Get the location of the mouse (in target ListView co-ords)
+ ///
+ public Point MouseLocation {
+ get { return this.mouseLocation; }
+ internal set { this.mouseLocation = value; }
+ }
+ private Point mouseLocation;
+
+ ///
+ /// Get the drop action indicated solely by the state of the modifier keys
+ ///
+ public DragDropEffects StandardDropActionFromKeys {
+ get {
+ return this.DropSink.CalculateStandardDropActionFromKeys();
+ }
+ }
+
+ #endregion
+ }
+
+ ///
+ /// These events are triggered when the drag source is an ObjectListView.
+ ///
+ public class ModelDropEventArgs : OlvDropEventArgs
+ {
+ ///
+ /// Create a ModelDropEventArgs
+ ///
+ public ModelDropEventArgs()
+ {
+ }
+
+ ///
+ /// Gets the model objects that are being dragged.
+ ///
+ public IList SourceModels {
+ get { return this.dragModels; }
+ internal set {
+ this.dragModels = value;
+ TreeListView tlv = this.SourceListView as TreeListView;
+ if (tlv != null) {
+ foreach (object model in this.SourceModels) {
+ object parent = tlv.GetParent(model);
+ if (!toBeRefreshed.Contains(parent))
+ toBeRefreshed.Add(parent);
+ }
+ }
+ }
+ }
+ private IList dragModels;
+ private ArrayList toBeRefreshed = new ArrayList();
+
+ ///
+ /// Gets the ObjectListView that is the source of the dragged objects.
+ ///
+ public ObjectListView SourceListView {
+ get { return this.sourceListView; }
+ internal set { this.sourceListView = value; }
+ }
+ private ObjectListView sourceListView;
+
+ ///
+ /// Get the model object that is being dropped upon.
+ ///
+ /// This is only value for TargetLocation == Item
+ public object TargetModel {
+ get { return this.targetModel; }
+ internal set { this.targetModel = value; }
+ }
+ private object targetModel;
+
+ ///
+ /// Refresh all the objects involved in the operation
+ ///
+ public void RefreshObjects() {
+
+ toBeRefreshed.AddRange(this.SourceModels);
+ TreeListView tlv = this.SourceListView as TreeListView;
+ if (tlv == null)
+ this.SourceListView.RefreshObjects(toBeRefreshed);
+ else
+ tlv.RebuildAll(true);
+
+ TreeListView tlv2 = this.ListView as TreeListView;
+ if (tlv2 == null)
+ this.ListView.RefreshObject(this.TargetModel);
+ else
+ tlv2.RebuildAll(true);
+ }
+ }
+}
diff --git a/ObjectListView/DragDrop/OLVDataObject.cs b/ObjectListView/DragDrop/OLVDataObject.cs
new file mode 100644
index 0000000..116861b
--- /dev/null
+++ b/ObjectListView/DragDrop/OLVDataObject.cs
@@ -0,0 +1,185 @@
+/*
+ * OLVDataObject.cs - An OLE DataObject that knows how to convert rows of an OLV to text and HTML
+ *
+ * Author: Phillip Piper
+ * Date: 2011-03-29 3:34PM
+ *
+ * Change log:
+ * v2.8
+ * 2014-05-02 JPP - When the listview is completely empty, don't try to set CSV text in the clipboard.
+ * v2.6
+ * 2012-08-08 JPP - Changed to use OLVExporter.
+ * - Added CSV to formats exported to Clipboard
+ * v2.4
+ * 2011-03-29 JPP - Initial version
+ *
+ * Copyright (C) 2011-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware {
+
+ ///
+ /// A data transfer object that knows how to transform a list of model
+ /// objects into a text and HTML representation.
+ ///
+ public class OLVDataObject : DataObject {
+ #region Life and death
+
+ ///
+ /// Create a data object from the selected objects in the given ObjectListView
+ ///
+ /// The source of the data object
+ public OLVDataObject(ObjectListView olv)
+ : this(olv, olv.SelectedObjects) {
+ }
+
+ ///
+ /// Create a data object which operates on the given model objects
+ /// in the given ObjectListView
+ ///
+ /// The source of the data object
+ /// The model objects to be put into the data object
+ public OLVDataObject(ObjectListView olv, IList modelObjects) {
+ this.objectListView = olv;
+ this.modelObjects = modelObjects;
+ this.includeHiddenColumns = olv.IncludeHiddenColumnsInDataTransfer;
+ this.includeColumnHeaders = olv.IncludeColumnHeadersInCopy;
+ this.CreateTextFormats();
+ }
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets or sets whether hidden columns will also be included in the text
+ /// and HTML representation. If this is false, only visible columns will
+ /// be included.
+ ///
+ public bool IncludeHiddenColumns {
+ get { return includeHiddenColumns; }
+ }
+ private readonly bool includeHiddenColumns;
+
+ ///
+ /// Gets or sets whether column headers will also be included in the text
+ /// and HTML representation.
+ ///
+ public bool IncludeColumnHeaders {
+ get { return includeColumnHeaders; }
+ }
+ private readonly bool includeColumnHeaders;
+
+ ///
+ /// Gets the ObjectListView that is being used as the source of the data
+ ///
+ public ObjectListView ListView {
+ get { return objectListView; }
+ }
+ private readonly ObjectListView objectListView;
+
+ ///
+ /// Gets the model objects that are to be placed in the data object
+ ///
+ public IList ModelObjects {
+ get { return modelObjects; }
+ }
+ private readonly IList modelObjects;
+
+ #endregion
+
+ ///
+ /// Put a text and HTML representation of our model objects
+ /// into the data object.
+ ///
+ public void CreateTextFormats() {
+
+ OLVExporter exporter = this.CreateExporter();
+
+ // Put both the text and html versions onto the clipboard.
+ // For some reason, SetText() with UnicodeText doesn't set the basic CF_TEXT format,
+ // but using SetData() does.
+ //this.SetText(sbText.ToString(), TextDataFormat.UnicodeText);
+ this.SetData(exporter.ExportTo(OLVExporter.ExportFormat.TabSeparated));
+ string exportTo = exporter.ExportTo(OLVExporter.ExportFormat.CSV);
+ if (!String.IsNullOrEmpty(exportTo))
+ this.SetText(exportTo, TextDataFormat.CommaSeparatedValue);
+ this.SetText(ConvertToHtmlFragment(exporter.ExportTo(OLVExporter.ExportFormat.HTML)), TextDataFormat.Html);
+ }
+
+ ///
+ /// Create an exporter for the data contained in this object
+ ///
+ ///
+ protected OLVExporter CreateExporter() {
+ OLVExporter exporter = new OLVExporter(this.ListView);
+ exporter.IncludeColumnHeaders = this.IncludeColumnHeaders;
+ exporter.IncludeHiddenColumns = this.IncludeHiddenColumns;
+ exporter.ModelObjects = this.ModelObjects;
+ return exporter;
+ }
+
+ ///
+ /// Make a HTML representation of our model objects
+ ///
+ [Obsolete("Use OLVExporter directly instead", false)]
+ public string CreateHtml() {
+ OLVExporter exporter = this.CreateExporter();
+ return exporter.ExportTo(OLVExporter.ExportFormat.HTML);
+ }
+
+ ///
+ /// Convert the fragment of HTML into the Clipboards HTML format.
+ ///
+ /// The HTML format is found here http://msdn2.microsoft.com/en-us/library/aa767917.aspx
+ ///
+ /// The HTML to put onto the clipboard. It must be valid HTML!
+ /// A string that can be put onto the clipboard and will be recognised as HTML
+ private string ConvertToHtmlFragment(string fragment) {
+ // Minimal implementation of HTML clipboard format
+ const string SOURCE = "http://www.codeproject.com/Articles/16009/A-Much-Easier-to-Use-ListView";
+
+ const String MARKER_BLOCK =
+ "Version:1.0\r\n" +
+ "StartHTML:{0,8}\r\n" +
+ "EndHTML:{1,8}\r\n" +
+ "StartFragment:{2,8}\r\n" +
+ "EndFragment:{3,8}\r\n" +
+ "StartSelection:{2,8}\r\n" +
+ "EndSelection:{3,8}\r\n" +
+ "SourceURL:{4}\r\n" +
+ "{5}";
+
+ int prefixLength = String.Format(MARKER_BLOCK, 0, 0, 0, 0, SOURCE, "").Length;
+
+ const String DEFAULT_HTML_BODY =
+ "" +
+ "{0}";
+
+ string html = String.Format(DEFAULT_HTML_BODY, fragment);
+ int startFragment = prefixLength + html.IndexOf(fragment, StringComparison.Ordinal);
+ int endFragment = startFragment + fragment.Length;
+
+ return String.Format(MARKER_BLOCK, prefixLength, prefixLength + html.Length, startFragment, endFragment, SOURCE, html);
+ }
+ }
+}
diff --git a/ObjectListView/FastDataListView.cs b/ObjectListView/FastDataListView.cs
new file mode 100644
index 0000000..8b30d2b
--- /dev/null
+++ b/ObjectListView/FastDataListView.cs
@@ -0,0 +1,169 @@
+/*
+ * FastDataListView - A data bindable listview that has the speed of a virtual list
+ *
+ * Author: Phillip Piper
+ * Date: 22/09/2010 8:11 AM
+ *
+ * Change log:
+ * 2015-02-02 JPP - Made Unfreezing more efficient by removing a redundant BuildList() call
+ * v2.6
+ * 2010-09-22 JPP - Initial version
+ *
+ * Copyright (C) 2006-2015 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.ComponentModel;
+using System.Windows.Forms;
+using System.Drawing.Design;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// A FastDataListView virtualizes the display of data from a DataSource. It operates on
+ /// DataSets and DataTables in the same way as a DataListView, but does so much more efficiently.
+ ///
+ ///
+ ///
+ /// A FastDataListView still has to load all its data from the DataSource. If you have SQL statement
+ /// that returns 1 million rows, all 1 million rows will still need to read from the database.
+ /// However, once the rows are loaded, the FastDataListView will only build rows as they are displayed.
+ ///
+ ///
+ public class FastDataListView : FastObjectListView
+ {
+ ///
+ ///
+ ///
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (this.adapter != null) {
+ this.adapter.Dispose();
+ this.adapter = null;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ #region Public Properties
+
+ ///
+ /// Gets or sets whether or not columns will be automatically generated to show the
+ /// columns when the DataSource is set.
+ ///
+ /// This must be set before the DataSource is set. It has no effect afterwards.
+ [Category("Data"),
+ Description("Should the control automatically generate columns from the DataSource"),
+ DefaultValue(true)]
+ public bool AutoGenerateColumns
+ {
+ get { return this.Adapter.AutoGenerateColumns; }
+ set { this.Adapter.AutoGenerateColumns = value; }
+ }
+
+ ///
+ /// Get or set the VirtualListDataSource that will be displayed in this list view.
+ ///
+ /// The VirtualListDataSource should implement either , ,
+ /// or . Some common examples are the following types of objects:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// When binding to a list container (i.e. one that implements the
+ /// interface, such as )
+ /// you must also set the property in order
+ /// to identify which particular list you would like to display. You
+ /// may also set the property even when
+ /// VirtualListDataSource refers to a list, since can
+ /// also be used to navigate relations between lists.
+ ///
+ [Category("Data"),
+ TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")]
+ public virtual Object DataSource {
+ get { return this.Adapter.DataSource; }
+ set { this.Adapter.DataSource = value; }
+ }
+
+ ///
+ /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data.
+ ///
+ /// If the data source is not a DataSet or DataViewManager, this property has no effect
+ [Category("Data"),
+ Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)),
+ DefaultValue("")]
+ public virtual string DataMember {
+ get { return this.Adapter.DataMember; }
+ set { this.Adapter.DataMember = value; }
+ }
+
+ #endregion
+
+ #region Implementation properties
+
+ ///
+ /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed
+ /// for data binding.
+ ///
+ protected DataSourceAdapter Adapter {
+ get {
+ if (adapter == null)
+ adapter = this.CreateDataSourceAdapter();
+ return adapter;
+ }
+ set { adapter = value; }
+ }
+ private DataSourceAdapter adapter;
+
+ #endregion
+
+ #region Implementation
+
+ ///
+ /// Create the DataSourceAdapter that this control will use.
+ ///
+ /// A DataSourceAdapter configured for this list
+ /// Subclasses should override this to create their
+ /// own specialized adapters
+ protected virtual DataSourceAdapter CreateDataSourceAdapter() {
+ return new DataSourceAdapter(this);
+ }
+
+ ///
+ /// Change the Unfreeze behaviour
+ ///
+ protected override void DoUnfreeze()
+ {
+
+ // Copied from base method, but we don't need to BuildList() since we know that our
+ // data adaptor is going to do that immediately after this method exits.
+ this.EndUpdate();
+ this.ResizeFreeSpaceFillingColumns();
+ // this.BuildList();
+ }
+
+ #endregion
+ }
+}
diff --git a/ObjectListView/FastObjectListView.cs b/ObjectListView/FastObjectListView.cs
new file mode 100644
index 0000000..0c5fe30
--- /dev/null
+++ b/ObjectListView/FastObjectListView.cs
@@ -0,0 +1,422 @@
+/*
+ * FastObjectListView - A listview that behaves like an ObjectListView but has the speed of a virtual list
+ *
+ * Author: Phillip Piper
+ * Date: 27/09/2008 9:15 AM
+ *
+ * Change log:
+ * 2014-10-15 JPP - Fire Filter event when applying filters
+ * v2.8
+ * 2012-06-11 JPP - Added more efficient version of FilteredObjects
+ * v2.5.1
+ * 2011-04-25 JPP - Fixed problem with removing objects from filtered or sorted list
+ * v2.4
+ * 2010-04-05 JPP - Added filtering
+ * v2.3
+ * 2009-08-27 JPP - Added GroupingStrategy
+ * - Added optimized Objects property
+ * v2.2.1
+ * 2009-01-07 JPP - Made all public and protected methods virtual
+ * 2008-09-27 JPP - Separated from ObjectListView.cs
+ *
+ * Copyright (C) 2006-2014 Phillip Piper
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Windows.Forms;
+
+namespace BrightIdeasSoftware
+{
+ ///
+ /// A FastObjectListView trades function for speed.
+ ///
+ ///
+ /// On my mid-range laptop, this view builds a list of 10,000 objects in 0.1 seconds,
+ /// as opposed to a normal ObjectListView which takes 10-15 seconds. Lists of up to 50,000 items should be
+ /// able to be handled with sub-second response times even on low end machines.
+ ///
+ /// A FastObjectListView is implemented as a virtual list with many of the virtual modes limits (e.g. no sorting)
+ /// fixed through coding. There are some functions that simply cannot be provided. Specifically, a FastObjectListView cannot:
+ ///
+ /// use Tile view
+ /// show groups on XP
+ ///
+ ///
+ ///
+ public class FastObjectListView : VirtualObjectListView
+ {
+ ///
+ /// Make a FastObjectListView
+ ///
+ public FastObjectListView() {
+ this.VirtualListDataSource = new FastObjectListDataSource(this);
+ this.GroupingStrategy = new FastListGroupingStrategy();
+ }
+
+ ///
+ /// Gets the collection of objects that survive any filtering that may be in place.
+ ///
+ [Browsable(false),
+ DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public override IEnumerable FilteredObjects {
+ get {
+ // This is much faster than the base method
+ return ((FastObjectListDataSource)this.VirtualListDataSource).FilteredObjectList;
+ }
+ }
+
+ ///
+ /// Get/set the collection of objects that this list will show
+ ///
+ ///
+ ///
+ /// The contents of the control will be updated immediately after setting this property.
+ ///
+ /// This method preserves selection, if possible. Use SetObjects() if
+ /// you do not want to preserve the selection. Preserving selection is the slowest part of this
+ /// code and performance is O(n) where n is the number of selected rows.
+ /// This method is not thread safe.
+ ///
+ [Browsable(false),
+ DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public override IEnumerable Objects {
+ get {
+ // This is much faster than the base method
+ return ((FastObjectListDataSource)this.VirtualListDataSource).ObjectList;
+ }
+ set { base.Objects = value; }
+ }
+
+ ///
+ /// Move the given collection of objects to the given index.
+ ///
+ /// This operation only makes sense on non-grouped ObjectListViews.
+ ///
+ ///
+ public override void MoveObjects(int index, ICollection modelObjects) {
+ if (this.InvokeRequired) {
+ this.Invoke((MethodInvoker)delegate() { this.MoveObjects(index, modelObjects); });
+ return;
+ }
+
+ // If any object that is going to be moved is before the point where the insertion
+ // will occur, then we have to reduce the location of our insertion point
+ int displacedObjectCount = 0;
+ foreach (object modelObject in modelObjects) {
+ int i = this.IndexOf(modelObject);
+ if (i >= 0 && i <= index)
+ displacedObjectCount++;
+ }
+ index -= displacedObjectCount;
+
+ this.BeginUpdate();
+ try {
+ this.RemoveObjects(modelObjects);
+ this.InsertObjects(index, modelObjects);
+ }
+ finally {
+ this.EndUpdate();
+ }
+ }
+
+ ///
+ /// Remove any sorting and revert to the given order of the model objects
+ ///
+ /// To be really honest, Unsort() doesn't work on FastObjectListViews since
+ /// the original ordering of model objects is lost when Sort() is called. So this method
+ /// effectively just turns off sorting.
+ public override void Unsort() {
+ this.ShowGroups = false;
+ this.PrimarySortColumn = null;
+ this.PrimarySortOrder = SortOrder.None;
+ this.SetObjects(this.Objects);
+ }
+ }
+
+ ///
+ /// Provide a data source for a FastObjectListView
+ ///
+ ///
+ /// This class isn't intended to be used directly, but it is left as a public
+ /// class just in case someone wants to subclass it.
+ ///
+ public class FastObjectListDataSource : AbstractVirtualListDataSource
+ {
+ ///
+ /// Create a FastObjectListDataSource
+ ///
+ ///
+ public FastObjectListDataSource(FastObjectListView listView)
+ : base(listView) {
+ }
+
+ #region IVirtualListDataSource Members
+
+ ///
+ /// Get n'th object
+ ///
+ ///
+ ///
+ public override object GetNthObject(int n) {
+ if (n >= 0 && n < this.filteredObjectList.Count)
+ return this.filteredObjectList[n];
+
+ return null;
+ }
+
+ ///
+ /// How many items are in the data source
+ ///
+ ///
+ public override int GetObjectCount() {
+ return this.filteredObjectList.Count;
+ }
+
+ ///
+ /// Get the index of the given model
+ ///
+ ///
+ ///
+ public override int GetObjectIndex(object model) {
+ int index;
+
+ if (model != null && this.objectsToIndexMap.TryGetValue(model, out index))
+ return index;
+
+ return -1;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override int SearchText(string text, int first, int last, OLVColumn column) {
+ if (first <= last) {
+ for (int i = first; i <= last; i++) {
+ string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject);
+ if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase))
+ return i;
+ }
+ } else {
+ for (int i = first; i >= last; i--) {
+ string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject);
+ if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase))
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override void Sort(OLVColumn column, SortOrder sortOrder) {
+ if (sortOrder != SortOrder.None) {
+ ModelObjectComparer comparer = new ModelObjectComparer(column, sortOrder, this.listView.SecondarySortColumn, this.listView.SecondarySortOrder);
+ this.fullObjectList.Sort(comparer);
+ this.filteredObjectList.Sort(comparer);
+ }
+ this.RebuildIndexMap();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override void AddObjects(ICollection modelObjects) {
+ foreach (object modelObject in modelObjects) {
+ if (modelObject != null)
+ this.fullObjectList.Add(modelObject);
+ }
+ this.FilterObjects();
+ this.RebuildIndexMap();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override void InsertObjects(int index, ICollection modelObjects) {
+ this.fullObjectList.InsertRange(index, modelObjects);
+ this.FilterObjects();
+ this.RebuildIndexMap();
+ }
+
+ ///
+ /// Remove the given collection of models from this source.
+ ///
+ ///
+ public override void RemoveObjects(ICollection modelObjects) {
+
+ // We have to unselect any object that is about to be deleted
+ List indicesToRemove = new List();
+ foreach (object modelObject in modelObjects) {
+ int i = this.GetObjectIndex(modelObject);
+ if (i >= 0)
+ indicesToRemove.Add(i);
+ }
+
+ // Sort the indices from highest to lowest so that we
+ // remove latter ones before earlier ones. In this way, the
+ // indices of the rows doesn't change after the deletes.
+ indicesToRemove.Sort();
+ indicesToRemove.Reverse();
+
+ foreach (int i in indicesToRemove)
+ this.listView.SelectedIndices.Remove(i);
+
+ // Remove the objects from the unfiltered list
+ foreach (object modelObject in modelObjects)
+ this.fullObjectList.Remove(modelObject);
+
+ this.FilterObjects();
+ this.RebuildIndexMap();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override void SetObjects(IEnumerable collection) {
+ ArrayList newObjects = ObjectListView.EnumerableToArray(collection, true);
+
+ this.fullObjectList = newObjects;
+ this.FilterObjects();
+ this.RebuildIndexMap();
+ }
+
+ ///
+ /// Update/replace the nth object with the given object
+ ///
+ ///
+ ///
+ public override void UpdateObject(int index, object modelObject) {
+ if (index < 0 || index >= this.filteredObjectList.Count)
+ return;
+
+ int i = this.fullObjectList.IndexOf(this.filteredObjectList[index]);
+ if (i < 0)
+ return;
+
+ if (ReferenceEquals(this.fullObjectList[i], modelObject))
+ return;
+
+ this.fullObjectList[i] = modelObject;
+ this.filteredObjectList[index] = modelObject;
+ this.objectsToIndexMap[modelObject] = index;
+ }
+
+ private ArrayList fullObjectList = new ArrayList();
+ private ArrayList filteredObjectList = new ArrayList();
+ private IModelFilter modelFilter;
+ private IListFilter listFilter;
+
+ #endregion
+
+ #region IFilterableDataSource Members
+
+ ///
+ /// Apply the given filters to this data source. One or both may be null.
+ ///
+ ///
+ ///
+ public override void ApplyFilters(IModelFilter iModelFilter, IListFilter iListFilter) {
+ this.modelFilter = iModelFilter;
+ this.listFilter = iListFilter;
+ this.SetObjects(this.fullObjectList);
+ }
+
+ #endregion
+
+ #region Implementation
+
+ ///
+ /// Gets the full list of objects being used for this fast list.
+ /// This list is unfiltered.
+ ///
+ public ArrayList ObjectList {
+ get { return fullObjectList; }
+ }
+
+ ///
+ /// Gets the list of objects from ObjectList which survive any installed filters.
+ ///
+ public ArrayList FilteredObjectList {
+ get { return filteredObjectList; }
+ }
+
+ ///
+ /// Rebuild the map that remembers which model object is displayed at which line
+ ///
+ protected void RebuildIndexMap() {
+ this.objectsToIndexMap.Clear();
+ for (int i = 0; i < this.filteredObjectList.Count; i++)
+ this.objectsToIndexMap[this.filteredObjectList[i]] = i;
+ }
+ readonly Dictionary