Code development platform for open source projects from the European Union institutions :large_blue_circle: EU Login authentication by SMS will be completely phased out by mid-2025. To see alternatives please check here

Skip to content
Snippets Groups Projects
Commit 3eadad40 authored by Markus Quaritsch's avatar Markus Quaritsch
Browse files

Merge branch 'feature/VECTO-53-read-and-write-datafiles' of...

Merge branch 'feature/VECTO-53-read-and-write-datafiles' of https://webgate.ec.europa.eu/CITnet/stash/scm/~emquarima/vecto-sim into feature/VECTO-57-implement-engine-model

Conflicts:
	VectoCoreTest/VectoCoreTest.csproj
	VectoCoreTest/app.config
parents 4afc161e 4e90d363
No related branches found
No related tags found
No related merge requests found
Showing with 528 additions and 85 deletions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TUGraz.VectoCore.Exceptions
{
class VectoException : Exception
{
public VectoException(string message) : base(message) { }
public VectoException(string message, Exception innerException) : base(message, innerException) { }
}
abstract class FileIOException : Exception
{
protected FileIOException(string message) : base(message)
{
abstract class FileIOException : VectoException
{
protected FileIOException(string message) : base(message) { }
protected FileIOException(string message, Exception inner) : base(message, inner) { }
}
}
/// <summary>
/// Exception which gets thrown when the version of a file is not supported.
/// </summary>
class UnsupportedFileVersion : FileIOException
{
public UnsupportedFileVersion(string message) : base(message) { }
public UnsupportedFileVersion(string message, Exception inner) : base(message, inner) { }
}
protected FileIOException(string message, Exception inner)
: base(message, inner)
{
}
}
class UnsupportedFileVersion : FileIOException
{
public UnsupportedFileVersion(string message) : base(message) { }
public UnsupportedFileVersion(string message, Exception inner) : base(message, inner) { }
}
/// <summary>
/// Exception which gets thrown when an error occurred during read of a vecto csv-file.
/// </summary>
class CSVReadException : FileIOException
{
public CSVReadException(string message) : base(message) { }
public CSVReadException(string message, Exception inner) : base(message, inner) { }
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using NLog.Layouts;
using TUGraz.VectoCore.Exceptions;
using TUGraz.VectoCore.Models.SimulationComponent.Data.Engine;
......@@ -46,6 +40,9 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
/// </code>
public class CombustionEngineData : SimulationComponentData
{
/// <summary>
/// A class which represents the json data format for serializing and deserializing the EngineData files.
/// </summary>
public class Data
{
public class DataHeader
......@@ -166,10 +163,6 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
private readonly Dictionary<string, FullLoadCurve> _fullLoadCurves = new Dictionary<string, FullLoadCurve>();
public static CombustionEngineData ReadFromFile(string fileName)
{
//todo: file exception handling: file not readable
......
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using TUGraz.VectoCore.Exceptions;
using TUGraz.VectoCore.Utils;
namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine
......@@ -32,27 +32,56 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data.Engine
public double FuelConsumption { get; set; }
}
private IList<FuelConsumptionEntry> entries;
private IList<FuelConsumptionEntry> _entries = new List<FuelConsumptionEntry>();
public FuelConsumptionMap(string fileName)
private DelauneyMap _fuelMap = new DelauneyMap();
private FuelConsumptionMap()
{
}
public static FuelConsumptionMap ReadFromFile(string fileName)
{
var fuelConsumptionMap = new FuelConsumptionMap();
var data = VectoCSVReader.Read(fileName);
entries = new List<FuelConsumptionEntry>();
//todo: catch exceptions if value format is wrong.
foreach (DataRow row in data.Rows)
try
{
var entry = new FuelConsumptionEntry();
entry.EngineSpeed = row.GetDouble(Fields.EngineSpeed);
entry.Torque = row.GetDouble(Fields.Torque);
entry.FuelConsumption = row.GetDouble(Fields.FuelConsumption);
entries.Add(entry);
foreach (DataRow row in data.Rows)
{
try
{
var entry = new FuelConsumptionEntry
{
EngineSpeed = row.GetDouble(Fields.EngineSpeed),
Torque = row.GetDouble(Fields.Torque),
FuelConsumption = row.GetDouble(Fields.FuelConsumption)
};
if (entry.FuelConsumption < 0)
throw new ArgumentOutOfRangeException("FuelConsumption < 0" + data.Rows.IndexOf(row));
fuelConsumptionMap._entries.Add(entry);
fuelConsumptionMap._fuelMap.AddPoints(entry.EngineSpeed, entry.Torque, entry.FuelConsumption);
}
catch (Exception e)
{
throw new VectoException(string.Format("Line {0}: {1}", data.Rows.IndexOf(row), e.Message), e);
}
}
}
catch (Exception e)
{
throw new VectoException(string.Format("File {0}: {1}", fileName, e.Message), e);
}
fuelConsumptionMap._fuelMap.Triangulate();
return fuelConsumptionMap;
}
public static FuelConsumptionMap ReadFromFile(string fileName)
public double GetFuelConsumption(double engineSpeed, double torque)
{
return new FuelConsumptionMap(fileName);
return _fuelMap.Interpolate(engineSpeed, torque);
}
}
}
......@@ -23,10 +23,10 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
{
private static class Fields
{
public const string Pe = "Pe";
public const string Padd = "Padd";
public const string Me = "Me";
public const string n = "n";
public const string PowerEngine = "Pe";
public const string PowerAuxilaries = "Padd";
public const string Torque = "Me";
public const string EngineSpeed = "n";
}
/// <summary>
......@@ -52,7 +52,7 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
/// <summary>
/// Additional power demand (aux) (Optional).
/// </summary>
public double Padd { get; set; }
public double PowerAuxilaries { get; set; }
public static List<EngineOnlyDrivingCycle> ReadFromFile(string fileName)
{
......@@ -64,15 +64,15 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
foreach (DataRow row in data.Rows)
{
var cycle = new EngineOnlyDrivingCycle();
cycle.EngineSpeed = row.GetDouble(Fields.n);
cycle.EngineSpeed = row.GetDouble(Fields.EngineSpeed);
if (data.Columns.Contains(Fields.Pe))
cycle.PowerEngine = row.GetDouble(Fields.Pe);
if (data.Columns.Contains(Fields.PowerEngine))
cycle.PowerEngine = row.GetDouble(Fields.PowerEngine);
else
cycle.Torque = row.GetDouble(Fields.Me);
cycle.Torque = row.GetDouble(Fields.Torque);
if (data.Columns.Contains(Fields.Padd))
cycle.Padd = row.GetDouble(Fields.Padd);
if (data.Columns.Contains(Fields.PowerAuxilaries))
cycle.PowerAuxilaries = row.GetDouble(Fields.PowerAuxilaries);
cycles.Add(cycle);
}
......
using System;
using System.Data;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using TUGraz.VectoCore.Exceptions;
namespace TUGraz.VectoCore.Models.SimulationComponent.Data
{
public static class VectoCSVReader
{
private const char Separator = ',';
private const char Comment = '#';
/// <summary>
/// Reads a CSV file which is stored in Vecto-CSV-Format.
/// </summary>
/// <param name="fileName"></param>
/// <exception cref="FileIOException"></exception>
/// <returns>A DataTable which represents the CSV File.</returns>
/// <remarks>
/// The following format applies to all CSV (Comma-separated values) Input Files used in VECTO:
......@@ -24,25 +30,51 @@ namespace TUGraz.VectoCore.Models.SimulationComponent.Data
/// </remarks>
public static DataTable Read(string fileName)
{
var lines = File.ReadAllLines(fileName);
var header = lines.First();
header = Regex.Replace(header, @"\[.*?\]", "");
header = Regex.Replace(header, @"\(.*?\)", "");
header = header.Replace("<", "");
header = header.Replace(">", "");
var cols = header.Split(',');
var table = new DataTable();
foreach (var col in cols)
table.Columns.Add(col.Trim(), typeof(string));
foreach (string line in lines.Skip(1))
try
{
//todo: do more sophisticated splitting of csv-columns (or use a good csv library!)
table.Rows.Add(line.Split(','));
}
var lines = File.ReadAllLines(fileName);
var header = lines.First();
header = Regex.Replace(header, @"\[.*?\]", "");
header = Regex.Replace(header, @"\(.*?\)", "");
header = header.Replace("<", "");
header = header.Replace(">", "");
// or all in one regex (incl. trim):
// Regex.Replace(header, @"\s*\[.*?\]\s*|\s*\(.*?\)\s*|\s*<|>\s*|\s*(?=,)|(?<=,)\s*", "");
var cols = header.Split(Separator);
var table = new DataTable();
foreach (var col in cols)
table.Columns.Add(col.Trim(), typeof(string));
// skip header! --> begin with index 1
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i];
//todo: do more sophisticated splitting of csv-columns (or use a good csv library!)
if (line.Contains(Comment))
line = line.Substring(0, line.IndexOf(Comment));
return table;
var cells = line.Split(Separator);
if (cells.Length != cols.Length)
throw new CSVReadException(string.Format("Line {0}: The number of values is not correct.", i));
try
{
table.Rows.Add(line.Split(Separator));
}
catch (InvalidCastException e)
{
throw new CSVReadException(string.Format("Line {0}: The data format of a value is not correct. {1}", i, e.Message), e);
}
}
return table;
}
catch (Exception e)
{
throw new VectoException(string.Format("File {0}: {1}", fileName, e.Message));
}
}
}
}
\ No newline at end of file
using System;
using System.Data;
using TUGraz.VectoCore.Exceptions;
namespace TUGraz.VectoCore.Utils
{
public static class DataRowExtensionMethods
{
/// <summary>
/// Gets the field of a DataRow as double.
/// </summary>
/// <param name="row"></param>
/// <param name="columnName">The name of the column.</param>
/// <returns>The value of the underlying DataRows column field as double.</returns>
public static double GetDouble(this DataRow row, string columnName)
{
return double.Parse(row.Field<string>(columnName));
//todo ArgumentNullException?
try
{
return double.Parse(row.Field<string>(columnName));
}
catch (IndexOutOfRangeException e)
{
throw new VectoException(string.Format("Field {0} was not found in DataRow.", columnName), e);
}
catch (NullReferenceException e)
{
throw new VectoException(string.Format("Field {0} must not be null.", columnName), e);
}
catch (FormatException e)
{
throw new VectoException(string.Format("Field {0} is not in a valid number format: {1}", columnName,
row.Field<string>(columnName)), e);
}
catch (OverflowException e)
{
throw new VectoException(string.Format("Field {0} has a value too high or too low: {1}", columnName,
row.Field<string>(columnName)), e);
}
catch (ArgumentNullException e)
{
throw new VectoException(string.Format("Field {0} contains null which cannot be converted to a number.", columnName), e);
}
catch (Exception e)
{
throw new VectoException(string.Format("Field {0}: {1}", columnName, e.Message), e);
}
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Win32;
using TUGraz.VectoCore.Exceptions;
namespace TUGraz.VectoCore.Utils
{
class DelauneyMap
{
private int ptDim;
private List<Point> ptList;
private List<Point> ptListXZ;
private List<Triangle> lDT;
private List<Triangle> lDTXZ;
private bool DualMode { get; set; }
public DelauneyMap(bool dualMode = false)
{
ptList = new List<Point>();
ptListXZ = new List<Point>();
DualMode = dualMode;
}
public void AddPoints(double x, double y, double z)
{
ptList.Add(new Point(x, y, z));
ptListXZ.Add(new Point(x, z, y));
}
public void Triangulate()
{
lDT = Triangulate(ptList);
lDTXZ = Triangulate(ptListXZ);
}
private List<Triangle> Triangulate(List<Point> points)
{
if (points.Count < 3)
throw new ArgumentException("Can not triangulate less than three vertices!");
var triangles = new List<Triangle>();
// The "supertriangle" which encompasses all triangulation points.
// This triangle initializes the algorithm and will be removed later.
Triangle superTriangle = SuperTriangle(points);
triangles.Add(superTriangle);
for (var i = 0; i < points.Count; i++)
{
var edges = new List<Edge>();
// If the actual vertex lies inside the circumcircle, then the three edges of the
// triangle are added to the edge buffer and the triangle is removed from list.
for (var j = triangles.Count - 1; j >= 0; j--)
{
var t = triangles[j];
if (t.ContainsInCircumcircle(points[i]) > 0)
{
edges.Add(new Edge(t.P1, t.P2));
edges.Add(new Edge(t.P2, t.P3));
edges.Add(new Edge(t.P3, t.P1));
triangles.RemoveAt(j);
}
}
// Remove duplicate edges. This leaves the convex hull of the edges.
// The edges in this convex hull are oriented counterclockwise!
for (var j = edges.Count - 2; j >= 0; j--)
{
for (var k = edges.Count - 1; k > j; k--)
{
if (edges[j] == edges[k])
{
edges.RemoveAt(k);
edges.RemoveAt(j);
k--;
}
}
}
// Generate new counterclockwise oriented triangles filling the "hole" in
// the existing triangulation. These triangles all share the actual vertex.
for (var j = 0; j < edges.Count; j++)
{
triangles.Add(new Triangle(edges[j].StartPoint, edges[j].EndPoint, points[i]));
}
}
// We don't want the supertriangle in the triangulation, so
// remove all triangles sharing a vertex with the supertriangle.
for (var i = triangles.Count - 1; i >= 0; i--)
{
if (triangles[i].SharesVertexWith(superTriangle))
triangles.RemoveAt(i);
}
return triangles;
}
public double Interpolate(double x, double y)
{
foreach (var tr in lDT)
{
if (IsInside(tr, x, y))
{
var plane = new Plane(tr);
return (plane.W - x * plane.X - y * plane.Y) / plane.Z;
}
}
foreach (var tr in lDT)
{
if (IsInside(tr, x, y, exact: false))
{
var plane = new Plane(tr);
return (plane.W - x * plane.X - y * plane.Y) / plane.Z;
}
}
throw new VectoException("Interpolation failed.");
}
public double InterpolateXZ(double x, double z)
{
foreach (var tr in lDTXZ)
{
if (IsInside(tr, x, z))
{
var plane = new Plane(tr);
return (plane.W - x * plane.X - z * plane.Y) / plane.Z;
}
}
foreach (var tr in lDTXZ)
{
if (IsInside(tr, x, z, exact: false))
{
var plane = new Plane(tr);
return (plane.W - x * plane.X - z * plane.Y) / plane.Z;
}
}
throw new VectoException("Interpolation failed.");
}
private bool IsInside(Triangle tr, double x, double y, bool exact = true)
{
var p = new Point(x, y);
var v0 = tr.P3 - tr.P1;
var v1 = tr.P2 - tr.P1;
var v2 = p - tr.P1;
var dot00 = v0.DotProduct(v0);
var dot01 = v0.DotProduct(v1);
var dot02 = v0.DotProduct(v2);
var dot11 = v1.DotProduct(v1);
var dot12 = v1.DotProduct(v2);
var invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01);
var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
if (exact)
return u >= 0 && v >= 0 && u + v <= 1;
const double tolerance = 0.001;
return u + tolerance >= 0 & v + tolerance >= 0 & u + v <= 1 + tolerance;
}
private Triangle SuperTriangle(List<Point> triangulationPoints)
{
double num1 = triangulationPoints[0].X;
int num2 = 1;
int num3 = checked(triangulationPoints.Count - 1);
int index = num2;
while (index <= num3)
{
double num4 = Math.Abs(triangulationPoints[index].X);
double num5 = Math.Abs(triangulationPoints[index].Y);
if (num4 > num1)
num1 = num4;
if (num5 > num1)
num1 = num5;
checked { ++index; }
}
Point pp1 = new Point(10.0 * num1, 0.0, 0.0);
Point pp2 = new Point(0.0, 10.0 * num1, 0.0);
Point pp3 = new Point(-10.0 * num1, -10.0 * num1, 0.0);
return new Triangle(ref pp1, ref pp2, ref pp3);
}
}
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
public Point(double x, double y, double z = 0)
{
X = x;
Y = y;
Z = z;
}
public static bool operator ==(Point left, Point right)
{
return left.X == right.X && left.Y == right.Y;
}
public static bool operator !=(Point left, Point right)
{
return !(left == right);
}
public static Point operator -(Point p1, Point p2)
{
return new Point(p1.X - p2.X, p1.Y - p2.Y, p1.Z - p2.Z);
}
/// <summary>
/// Ex-Product or Vectorial Product
/// </summary>
/// <param name="p1"></param>
/// <param name="p2"></param>
/// <returns></returns>
public static Point operator *(Point p1, Point p2)
{
return new Point(p1.Y * p2.Z - p1.Z * p2.Y,
p1.Z * p2.X - p1.X * p2.Z,
p1.X * p2.Y - p1.Y * p2.X);
}
public double DotProduct(Point p1)
{
return X * p1.X + Y * p1.Y + Z * p1.Z;
}
}
public class Plane
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
public double W { get; set; }
public Plane(double x, double y, double z, double w)
{
X = x;
Y = y;
Z = z;
W = w;
}
public Plane(Triangle tr) : this(tr.P1, tr.P2, tr.P3) { }
public Plane(Point p1, Point p2, Point p3)
{
var prod = (p2 - p1) * (p3 - p1);
X = prod.X;
Y = prod.Y;
Z = prod.Z;
W = p1.DotProduct(prod);
}
}
public class Triangle
{
public Point P1;
public Point P2;
public Point P3;
public Triangle(Point p1, Point p2, Point p3)
{
P1 = p1;
P2 = p2;
P3 = p3;
}
public double ContainsInCircumcircle(Point pt)
{
double num1 = P1.X - pt.X;
double num2 = P1.Y - pt.Y;
double num3 = P2.X - pt.X;
double num4 = P2.Y - pt.Y;
double num5 = P3.X - pt.X;
double num6 = P3.Y - pt.Y;
double num7 = num1 * num4 - num3 * num2;
double num8 = num3 * num6 - num5 * num4;
double num9 = num5 * num2 - num1 * num6;
double num10 = num1 * num1 + num2 * num2;
double num11 = num3 * num3 + num4 * num4;
double num12 = num5 * num5 + num6 * num6;
return num10 * num8 + num11 * num9 + num12 * num7;
}
public bool SharesVertexWith(Triangle triangle)
{
return P1.X == triangle.P1.X && P1.Y == triangle.P1.Y
|| P1.X == triangle.P2.X && P1.Y == triangle.P2.Y
|| (P1.X == triangle.P3.X && P1.Y == triangle.P3.Y || P2.X == triangle.P1.X && P2.Y == triangle.P1.Y)
|| (P2.X == triangle.P2.X && P2.Y == triangle.P2.Y
|| P2.X == triangle.P3.X && P2.Y == triangle.P3.Y
|| (P3.X == triangle.P1.X && P3.Y == triangle.P1.Y || P3.X == triangle.P2.X && P3.Y == triangle.P2.Y))
|| P3.X == triangle.P3.X && P3.Y == triangle.P3.Y;
}
}
public class Edge
{
public Point StartPoint;
public Point EndPoint;
public Edge(Point p1, Point p2)
{
StartPoint = p1;
EndPoint = p2;
}
public static bool operator ==(Edge left, Edge right)
{
return left.StartPoint == right.StartPoint && left.EndPoint == right.EndPoint || left.StartPoint == right.EndPoint && left.EndPoint == right.StartPoint;
}
public static bool operator !=(Edge left, Edge right)
{
return !(left == right);
}
}
}
......@@ -86,6 +86,8 @@
<Compile Include="Models\SimulationComponent\VectoSimulationComponent.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Utils\DataRowExtensionMethods.cs" />
<Compile Include="Utils\VectoMath.cs" />
<Compile Include="Utils\DelauneyMap.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
......
......@@ -77,8 +77,8 @@ namespace TUGraz.VectoCore.Tests.Models.SimulationComponent
//todo: test with correct output values, add other fields to test
Assert.AreEqual(dataWriter[ModalResultField.FC], 13000);
Assert.AreEqual(dataWriter[ModalResultField.FC_AUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FC_WHTCc], 15000);
Assert.AreEqual(dataWriter[ModalResultField.FCAUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FCWHTCc], 15000);
}
[TestMethod]
......@@ -103,14 +103,14 @@ namespace TUGraz.VectoCore.Tests.Models.SimulationComponent
//todo: test with correct output values, add other fields to test
Assert.AreEqual(dataWriter[ModalResultField.FC], 13000);
Assert.AreEqual(dataWriter[ModalResultField.FC_AUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FC_WHTCc], 15000);
Assert.AreEqual(dataWriter[ModalResultField.FCAUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FCWHTCc], 15000);
}
//todo: test with correct output values, add other fields to test
Assert.AreEqual(dataWriter[ModalResultField.FC], 13000);
Assert.AreEqual(dataWriter[ModalResultField.FC_AUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FC_WHTCc], 15000);
Assert.AreEqual(dataWriter[ModalResultField.FCAUXc], 14000);
Assert.AreEqual(dataWriter[ModalResultField.FCWHTCc], 15000);
}
public TestContext TestContext { get; set; }
......
......@@ -84,24 +84,29 @@
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
<None Include="default.runsettings" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<None Include="TestData\EngineOnly\Test1\24t Coach.veng">
<None Include="TestData\EngineOnly\EngineMaps\24t Coach.veng">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="TestData\EngineOnly\Test1\24t Coach.vfld">
<None Include="TestData\EngineOnly\EngineMaps\24t Coach.vfld">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="TestData\EngineOnly\Test1\24t Coach.vmap">
<None Include="TestData\EngineOnly\EngineMaps\24t Coach.vmap">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="TestData\EngineOnly\Test1\Coach Engine Only.vdri">
<None Include="TestData\EngineOnly\Cycles\Coach Engine Only.vdri">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Include="TestData\EngineOnly\ResultFiles\Test1_results.vmod" />
<None Include="TestData\EngineOnly\ResultFiles\Test1_results.vsum" />
<None Include="TestData\EngineOnly\ResultFiles\Test1_results.vsum.json" />
<None Include="TestData\EngineTests.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
......
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="common">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment