Virtual datasets (C# / netframework)
Note
This demo is available in your FlexCel installation at <FlexCel Install Folder>\samples\csharp\VS2022\netframework\20.Reports\96.Virtual Datasets and also at https://github.com/tmssoftware/TMS-FlexCel.NET-demos/tree/master/csharp/VS2022/netframework/Modules/20.Reports/96.Virtual Datasets
Overview
Normally FlexCelReport uses DataTables for its data. On reporting we can see DataTables as "overcharged" arrays, with added functionality like filtering or sorting. They are also really powerful and fast, so using them is normally the best option. But in some cases you might have very large bussiness objects and would like to use them directly without copying them first into a DataTable. On those cases, you can create your own VirtualDataset and VirtualDatasetState descendants to do this task.
This is an advanced topic. Remember to read the [Appendix: Virtual DataSets on](~/guides/reports-developer-guide.md#appendix:-virtual-datasets -on) the Reports developer guide.
Concepts
How to create a VirtualDataset descendant to encapsulate an arbitrary object and make a report with it. Two descendants are shown, one very simple with the minimum functionality needed, and other that fully implements all the features.
As you can see on the ComplexVirtualArrayDataSource example, to provide all the functionality you do not need to override too many methods. But you should implement very efficient methods. If you do not, probably the performance dumping everything to a DataSet would be faster.
How to create "infographics". this is based on the article: http://www.juiceanalytics.com/weblog/?p=236
Files
AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
//
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
//
[assembly: AssemblyTitle("")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("(c) 2002 - 2024 TMS Software")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
//
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("7.21.0.0")]
//
// In order to sign your assembly you must specify a key to use. Refer to the
// Microsoft .NET Framework documentation for more information on assembly signing.
//
// Use the attributes below to control which key is used for signing.
//
// Notes:
// (*) If no key is specified, the assembly is not signed.
// (*) KeyName refers to a key that has been installed in the Crypto Service
// Provider (CSP) on your machine. KeyFile refers to a file which contains
// a key.
// (*) If the KeyFile and the KeyName values are both specified, the
// following processing occurs:
// (1) If the KeyName can be found in the CSP, that key is used.
// (2) If the KeyName does not exist and the KeyFile does exist, the key
// in the KeyFile is installed into the CSP and used.
// (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
// When specifying the KeyFile, the location of the KeyFile should be
// relative to the project output directory which is
// %Project Directory%\obj\<configuration>. For example, if your KeyFile is
// located in the project directory, you would specify the AssemblyKeyFile
// attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")]
// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
// documentation for more information on this.
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]
ComplexVirtualArrayDataSource.cs
using System;
using System.Globalization;
using System.Collections.Generic;
using FlexCel.Report;
using System.Collections;
namespace VirtualDatasets
{
/// <summary>
/// This class implements the complete functionality needed to run a FlexCelReport from an array of objects.
/// Sorting/Filtering on the config sheet is allowed for this datasource, but they are not implemented on an efficient way.
/// If you do not have an efficient way to do those things (using indexes) and you plan to do them, you will probably get better performance
/// using Datasets.
/// </summary>
public class ComplexVirtualArrayDataSource: VirtualDataTable
{
#region Private variables
object[][] FData;
string[] FColumnCaptions;
#endregion
#region Constructors
public ComplexVirtualArrayDataSource(VirtualDataTable aCreatedBy, object[][] aData, string[] aColumnCaptions, string aTableName)
: base(aTableName, aCreatedBy)
{
FData = aData;
FColumnCaptions = aColumnCaptions;
}
#endregion
#region Columns
public override int ColumnCount
{
get
{
return FColumnCaptions.Length;
}
}
public override int GetColumn(string columnName)
{
//not very optimized method, but this is just a demo.
if (columnName == null) return -1;
columnName = columnName.Trim();
for (int i = 0; i < FColumnCaptions.Length; i++)
{
if (string.Compare(FColumnCaptions[i], columnName, true) == 0) return i;
}
return -1;
}
public override string GetColumnName(int columnIndex)
{
return FColumnCaptions[columnIndex];
}
public override string GetColumnCaption(int columnIndex)
{
return FColumnCaptions[columnIndex];
}
#endregion
#region Settings
public override System.Globalization.CultureInfo Locale
{
get
{
return CultureInfo.CurrentCulture;
}
}
#endregion
#region Create State
public override VirtualDataTableState CreateState(string sort, TMasterDetailLink[] masterDetailLinks, TSplitLink splitLink)
{
return new ComplexVirtualArrayDataSourceState(this, sort, masterDetailLinks, splitLink);
}
#endregion
#region Data
public object[][] Data { get { return FData; } }
public override VirtualDataTable FilterData(string newDataName, string rowFilter)
{
if (rowFilter == null || rowFilter.Length == 0)
{
return new ComplexVirtualArrayDataSource(this, FData, FColumnCaptions, newDataName); //no need to copy the data since it is invariant.
}
RelationshipType Relationship = RelationshipType.Equals;
//on this demo we will only support filters of the type: "field = value", "field > value" or "field < value"
string[] filteredData = rowFilter.Split('=');
if (filteredData == null || filteredData.Length != 2)
{
filteredData = rowFilter.Split('<');
if (filteredData == null || filteredData.Length != 2)
{
filteredData = rowFilter.Split('>');
if (filteredData == null || filteredData.Length != 2)
{
throw new Exception("Filter \"" + rowFilter + "\" is invalid. The dataset \"" + TableName + "\" only supports filters of the type \"field =/>/< value\".");
}
else
{
Relationship = RelationshipType.BiggerThan;
}
}
else
{
Relationship = RelationshipType.LessThan;
}
}
int ColIndex = GetColumn(filteredData[0].Trim());
if (ColIndex < 0)
{
throw new Exception("Filter \"" + rowFilter + "\" is invalid. Can not find column \"" + filteredData[0].Trim() + "\"");
}
//Remember, this is only a demo to show what to do in this event. This code is not good code to use on
//a real application!. You should use some index here to find the data or this would crawl on large objects.
List<object[]> Result = new List<object[]>();
string SearchValue = filteredData[1].Trim();
double Searchfloat = 0;
if (Relationship != RelationshipType.Equals) //when relationship is not equals, we will assume columns are numbers. This is because we do not have any type definition for our columns on this simple example.
{
Searchfloat = Convert.ToDouble(filteredData[1].Trim());
}
foreach (object[] Row in Data)
{
switch (Relationship)
{
case RelationshipType.Equals:
if (Convert.ToString(Row[ColIndex]) == SearchValue)
{
Result.Add(Row); //remember, data is invariant, so we do not need to clone Row.
}
break;
case RelationshipType.LessThan:
if (Convert.ToDouble(Row[ColIndex]) < Searchfloat)
{
Result.Add(Row); //remember, data is invariant, so we do not need to clone Row.
}
break;
case RelationshipType.BiggerThan:
if (Convert.ToDouble(Row[ColIndex]) > Searchfloat)
{
Result.Add(Row); //remember, data is invariant, so we do not need to clone Row.
}
break;
}
}
return new ComplexVirtualArrayDataSource(this, Result.ToArray(), FColumnCaptions, newDataName);
}
public override VirtualDataTable GetDistinct(string newDataName, int[] filterFields)
{
if (filterFields == null || filterFields.Length == 0)
{
return new ComplexVirtualArrayDataSource(this, FData, FColumnCaptions, newDataName); //no need to copy the data since it is invariant.
}
Dictionary<object[], object[]> Result = new Dictionary<object[], object[]>();
object[] Keys = new object[filterFields.Length];
foreach (object[] Row in FData)
{
for (int i = 0; i < filterFields.Length; i++)
{
Keys[i] = Row[filterFields[i]];
}
Result[Keys] = Keys;
}
object[][] R = new object[Data.Length][];
Result.Keys.CopyTo(R, 0);
string[] NewColumnCaptions = new string[filterFields.Length];
for (int i = 0; i < filterFields.Length; i++)
{
NewColumnCaptions[i] = FColumnCaptions[filterFields[i]];
}
return new ComplexVirtualArrayDataSource(this, R, NewColumnCaptions, newDataName);
}
#endregion
}
public class ComplexVirtualArrayDataSourceState: VirtualDataTableState
{
#region Privates
private object[][] SortedData;
private List<object[]> FilteredData;
#endregion
#region Constructors
public ComplexVirtualArrayDataSourceState(ComplexVirtualArrayDataSource aTableData, string sort, TMasterDetailLink[] masterDetailLinks, TSplitLink splitLink)
: base(aTableData)
{
if (sort == null || sort.Trim().Length == 0)
{
SortedData = aTableData.Data; //no need to clone, this is invariant.
}
else
{
SortedData = (object[][])aTableData.Data.Clone();
int sortcolumn = aTableData.GetColumn(sort);
if (sortcolumn < 0)
{
throw new Exception("Can not find column \"" + sort + "\" in dataset \"" + TableName);
}
Array.Sort(SortedData, new ArrayComparer(sortcolumn));
}
//here we should use the data in masterdetaillinks and splitlink to create indexes to make the FilteredrowCount and MoveMasterRecord methods faster.
//on this demo we are not going to do it.
if ((masterDetailLinks != null && masterDetailLinks.Length > 0) || splitLink != null)
{
FilteredData = new List<object[]>();
}
}
#endregion
#region Data
private object[][] Data { get { return ((SimpleVirtualArrayDataSource)TableData).Data; } }
/// <summary>
/// Remember that this method should be fast!
/// </summary>
public override int RowCount
{
get
{
if (FilteredData == null) return SortedData.Length;
return FilteredData.Count;
}
}
public override object GetValue(int column)
{
if (FilteredData == null) return SortedData[Position][column];
return ((object[])FilteredData[Position])[column];
}
public override object GetValue(int row, int column)
{
if (FilteredData == null) return SortedData[row][column];
return ((object[])FilteredData[row])[column];
}
#endregion
#region Move
public override void MoveFirst()
{
//No need to do anything in an array, since "Position" is moved for us.
//If we had an IEnumerator for example as data backend, we would reset it here.
}
public override void MoveNext()
{
//No need to do anything in an array, since "Position" is moved for us.
//If we had an IEnumerator for example as data backend, we would move it here.
}
#endregion
#region Relationships
public override void MoveMasterRecord(TMasterDetailLink[] masterDetailLinks, TSplitLink splitLink)
{
//Here we need to modify FilteredData to contain only those records on FilteredData that are visible on this state.
//If we created indexes on this method constructor's, this can be done fast.
//on this example, as always, we will use a very slow method. The idea of this demo is not to demostrate how to do efficient code
// (you probably have efficient methods on the bussines objects you are wrapping), but how to override the methods.
if (FilteredData == null) return; //this dataset is not on master-detail relationship and does not have any split relationship either.
FilteredData.Clear();
int[] ChildColumn = new int[masterDetailLinks.Length];
for (int i = 0; i < masterDetailLinks.Length; i++)
{
ChildColumn[i] = TableData.GetColumn(masterDetailLinks[i].ChildFieldName);
}
int SplitPos = 0;
int StartRow = 0;
if (splitLink != null)
{
StartRow = splitLink.SplitCount * splitLink.ParentDataSource.Position;
}
for (int r = 0; r < SortedData.Length; r++)
{
object[] Row = SortedData[r];
bool RowApplies = true;
for (int i = 0; i < masterDetailLinks.Length; i++)
{
object key = masterDetailLinks[i].ParentDataSource.GetValue(masterDetailLinks[i].ParentField);
if (Convert.ToString(Row[ChildColumn[i]]) != Convert.ToString(key))
{
RowApplies = false;
break;
}
}
if (!RowApplies) continue; //the row does not fit this master detail relationship.
SplitPos++;
if (SplitPos <= StartRow) continue; //we are not filling the correct split slot.
FilteredData.Add(Row);
if (splitLink != null && FilteredData.Count >= splitLink.SplitCount) return;
}
}
public override int FilteredRowCount(TMasterDetailLink[] masterDetailLinks)
{
int Result = 0;
int[] ChildColumn = new int[masterDetailLinks.Length];
for (int i = 0; i < masterDetailLinks.Length; i++)
{
ChildColumn[i] = TableData.GetColumn(masterDetailLinks[i].ChildFieldName);
}
for (int r = 0; r < SortedData.Length; r++)
{
object[] Row = SortedData[r];
bool RowApplies = true;
for (int i = 0; i < masterDetailLinks.Length; i++)
{
object key = masterDetailLinks[i].ParentDataSource.GetValue(masterDetailLinks[i].ParentField);
if (Convert.ToString(Row[ChildColumn[i]]) != Convert.ToString(key))
{
RowApplies = false;
break;
}
}
if (!RowApplies) continue; //the row does not fit this master detail relationship.
Result++;
}
return Result;
}
#endregion
}
internal enum RelationshipType
{
Equals,
LessThan,
BiggerThan
}
public class ArrayComparer: IComparer<object[]>
{
int Column;
static CaseInsensitiveComparer Comparer = new CaseInsensitiveComparer();
public ArrayComparer(int aColumn)
{
Column = aColumn;
}
int IComparer<object[]>.Compare(Object[] x, Object[] y)
{
return (Comparer.Compare(x[Column], y[Column]));
}
}
}
Form1.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Diagnostics;
using System.Reflection;
using System.Resources;
using System.Globalization;
using FlexCel.Core;
using FlexCel.XlsAdapter;
using FlexCel.Report;
namespace VirtualDatasets
{
public partial class mainForm: System.Windows.Forms.Form
{
public mainForm()
{
InitializeComponent();
}
private void button1_Click(object sender, System.EventArgs e)
{
AutoRun();
}
public void AutoRun()
{
string DataPath = Path.Combine(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), ".."), "..") + Path.DirectorySeparatorChar;
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
object[][] SimpleData = LoadDataSet(Path.Combine(DataPath, "Countries.txt"));
SimpleVirtualArrayDataSource SimpleTable = new SimpleVirtualArrayDataSource(null, SimpleData, new string[] { "Rank", "Country", "Area", "Date" }, "SimpleTable");
using (FlexCelReport genericReport = new FlexCelReport(true))
{
genericReport.AddTable("SimpleData", SimpleTable);
object[][] Complex1 = LoadDataSet(Path.Combine(DataPath, "Countries.txt"));
ComplexVirtualArrayDataSource ComplexAreas = new ComplexVirtualArrayDataSource(null, Complex1, new string[] { "Rank", "Country", "Area", "Date" }, "ComplexAreas");
object[][] Complex2 = LoadDataSet(Path.Combine(DataPath, "Populations.txt"));
ComplexVirtualArrayDataSource ComplexPopulations = new ComplexVirtualArrayDataSource(null, Complex2, new string[] { "Rank", "Country", "Population", "Date" }, "ComplexPopulations");
genericReport.AddTable("ComplexAreas", ComplexAreas, TDisposeMode.DisposeAfterRun);
genericReport.AddTable("ComplexPopulations", ComplexPopulations, TDisposeMode.DisposeAfterRun);
genericReport.Run(Path.Combine(DataPath, "Virtual Datasets.template.xls"), saveFileDialog1.FileName);
}
if (MessageBox.Show("Do you want to open the generated file?", "Confirm", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
using (Process p = new Process())
{
p.StartInfo.FileName = saveFileDialog1.FileName;
p.StartInfo.UseShellExecute = true;
p.Start();
}
}
}
}
private void btnCancel_Click(object sender, System.EventArgs e)
{
Close();
}
private object[][] LoadDataSet(string filename)
{
//Let's create some bussiness object with random data.
ArrayList Result = new ArrayList();
using (StreamReader sr = new StreamReader(Path.GetFullPath(filename)))
{
string line;
while ((line = sr.ReadLine()) != null)
{
string[] fields = line.Split('\t');
//Zero validation here since this is a demo and will use always the same data. On a real app you should not expect your data to play nice
object[] f = new object[fields.Length];
string s = fields[0] as String;
f[0] = Convert.ToInt64(s);
f[1] = fields[1];
s = fields[2] as String;
f[2] = (object)Convert.ToInt64(s.Replace(",", ""));
f[3] = fields[3];
Result.Add(f);
}
}
return (object[][])Result.ToArray(typeof(object[]));
}
}
}
Form1.Designer.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Diagnostics;
using System.Reflection;
using System.Resources;
using System.Globalization;
using FlexCel.Core;
using FlexCel.XlsAdapter;
using FlexCel.Report;
namespace VirtualDatasets
{
public partial class mainForm: System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.SaveFileDialog saveFileDialog1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Button btnCancel;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.saveFileDialog1 = new System.Windows.Forms.SaveFileDialog();
this.label1 = new System.Windows.Forms.Label();
this.btnCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// button1
//
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.button1.BackColor = System.Drawing.Color.Green;
this.button1.ForeColor = System.Drawing.Color.White;
this.button1.Location = new System.Drawing.Point(96, 80);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(112, 23);
this.button1.TabIndex = 0;
this.button1.Text = "GO!";
this.button1.UseVisualStyleBackColor = false;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// saveFileDialog1
//
this.saveFileDialog1.Filter = "Excel Files|*.xls;*.xlsx;*.xlsm|Excel 97/2003|*.xls|Excel 2007|*.xlsx;*.xlsm|All files|*.*";
this.saveFileDialog1.RestoreDirectory = true;
//
// label1
//
this.label1.Location = new System.Drawing.Point(24, 24);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(288, 24);
this.label1.TabIndex = 2;
this.label1.Text = "A demo on how to use arbitrary objects on a report. ";
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(0)))), ((int)(((byte)(0)))));
this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.btnCancel.ForeColor = System.Drawing.Color.White;
this.btnCancel.Location = new System.Drawing.Point(216, 80);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(112, 23);
this.btnCancel.TabIndex = 3;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = false;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// mainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(360, 130);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Name = "mainForm";
this.Text = "Virtual Datasets";
this.ResumeLayout(false);
}
#endregion
}
}
Program.cs
using System;
using System.Windows.Forms;
namespace VirtualDatasets
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new mainForm());
}
}
}
SimpleVirtualArrayDataSource.cs
using System;
using System.Globalization;
using FlexCel.Report;
namespace VirtualDatasets
{
/// <summary>
/// This class implements the minimum functionality needed to run a FlexCelReport from an array of objects.
/// No Sorting/Filtering on the config sheet is allowed for this datasource, and you can not use it on master detail relationships.
/// </summary>
public class SimpleVirtualArrayDataSource: VirtualDataTable
{
#region Private variables
object[][] FData;
string[] FColumnCaptions;
#endregion
#region Constructors
public SimpleVirtualArrayDataSource(VirtualDataTable aCreatedBy, object[][] aData, string[] aColumnCaptions, string aTableName) : base(aTableName, aCreatedBy)
{
FData = aData;
FColumnCaptions = aColumnCaptions;
}
#endregion
#region Columns
public override int ColumnCount
{
get
{
return FColumnCaptions.Length;
}
}
public override int GetColumn(string columnName)
{
//not very optimized method, but this is just a demo.
if (columnName == null) return -1;
columnName = columnName.Trim();
for (int i = 0; i < FColumnCaptions.Length; i++)
{
if (string.Compare(FColumnCaptions[i], columnName, true) == 0) return i;
}
return -1;
}
public override string GetColumnName(int columnIndex)
{
return FColumnCaptions[columnIndex];
}
public override string GetColumnCaption(int columnIndex)
{
return FColumnCaptions[columnIndex];
}
#endregion
#region Settings
public override System.Globalization.CultureInfo Locale
{
get
{
return CultureInfo.CurrentCulture;
}
}
#endregion
#region Create State
public override VirtualDataTableState CreateState(string sort, TMasterDetailLink[] masterDetailLinks, TSplitLink splitLink)
{
return new SimpleVirtualArrayDataSourceState(this);
}
#endregion
#region Data
public object[][] Data { get { return FData; } }
#endregion
#region Filter
/// <summary>
/// Even when we don't implement filter, we need to return a new instance when rowFilter is null.
/// </summary>
/// <param name="newDataName"></param>
/// <param name="rowFilter"></param>
/// <returns></returns>
public override VirtualDataTable FilterData(string newDataName, string rowFilter)
{
if (string.IsNullOrEmpty(rowFilter))
{
return new SimpleVirtualArrayDataSource(this, Data, FColumnCaptions, TableName);
}
return base.FilterData(newDataName, rowFilter);
}
#endregion
}
public class SimpleVirtualArrayDataSourceState: VirtualDataTableState
{
#region Constructors
public SimpleVirtualArrayDataSourceState(SimpleVirtualArrayDataSource aTableData) : base(aTableData)
{
}
#endregion
#region Data
private object[][] Data { get { return ((SimpleVirtualArrayDataSource)TableData).Data; } }
/// <summary>
/// Remember that this method should be fast!
/// </summary>
public override int RowCount
{
get
{
return Data.Length;
}
}
public override object GetValue(int column)
{
return Data[Position][column];
}
public override object GetValue(int row, int column)
{
return Data[row][column];
}
public override void MoveFirst()
{
//No need to do anything in an array, since "Position" is moved for us.
//If we had an IEnumerator for example as data backend, we would reset it here.
}
public override void MoveNext()
{
//No need to do anything in an array, since "Position" is moved for us.
//If we had an IEnumerator for example as data backend, we would move it here.
}
#endregion
}
}