it-swarm.com.de

Verwenden von DataTable in .NET Core

Ich habe eine gespeicherte Prozedur in SQL Server, die einen benutzerdefinierten Tabellentyp akzeptiert. Ich folge der Antwort aus diesem Beitrag Masseneinfügung aus C # -Liste in SQL Server in mehrere Tabellen mit Fremdschlüsselkonstanten zum Senden einer DataTable an eine gespeicherte Prozedur in SQL.

Wenn ich jedoch DataTable table = new DataTable(); erstelle, erhalte ich die Fehlermeldung DataTable does not contain a constructor that takes 0 arguments.

Ich habe diese https://github.com/VahidN/EPPlus.Core/issues/4 gefunden, die im Grunde gesagt DataTable nicht mehr in .NET Core unterstützt. Also was jetzt? Wie erstelle ich eine DataTable (oder was ist die Ersetzung)? Wie sende ich einen benutzerdefinierten Tabellentyp an SQL Server in .NET Core?

11
developer82

DataTable wird jetzt in .NET CORE 2.0 unterstützt. In meiner Antwort unter .Net Core finden Sie Informationen zur Implementierung von SQLAdapter ./ DataTable-Funktion . Der folgende Beispielcode funktioniert in 2.0.

public static DataTable ExecuteDataTableSqlDA(SqlConnection conn, CommandType cmdType, string cmdText, SqlParameter[] cmdParms)
{
   System.Data.DataTable dt = new DataTable();
   System.Data.SqlClient.SqlDataAdapter da = new SqlDataAdapter(cmdText, conn);
   da.Fill(dt);
   return dt;
}
11
Joe Healy

Sie können eine DbDataReader als Wert des SQL-Parameters verwenden. Die Idee ist also, einen IEnumerable<T> in eine DbDataReader zu konvertieren.

public class ObjectDataReader<T> : DbDataReader
{
    private bool _iteratorOwned;
    private IEnumerator<T> _iterator;
    private IDictionary<string, int> _propertyNameToOrdinal = new Dictionary<string, int>();
    private IDictionary<int, string> _ordinalToPropertyName = new Dictionary<int, string>();
    private Func<T, object>[] _getPropertyValueFuncs;

    public ObjectDataReader(IEnumerable<T> enumerable)
    {
        if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));

        _iteratorOwned = true;
        _iterator = enumerable.GetEnumerator();
        _iterator.MoveNext();
        Initialize();
    }

    public ObjectDataReader(IEnumerator<T> iterator)
    {
        if (iterator == null) throw new ArgumentNullException(nameof(iterator));

        _iterator = iterator;    
        Initialize();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && _iteratorOwned)
        {
            if(_iterator != null)
                _iterator.Dispose();
        }

        base.Dispose(disposing);
    }

    private void Initialize()
    {
        int ordinal = 0;
        var properties = typeof(T).GetProperties();
        _getPropertyValueFuncs = new Func<T, object>[properties.Length];
        foreach (var property in properties)
        {
            string propertyName = property.Name;
            _propertyNameToOrdinal.Add(propertyName, ordinal);
            _ordinalToPropertyName.Add(ordinal, propertyName);

            var parameterExpression = Expression.Parameter(typeof(T), "x");
            var func = (Func<T, object>)Expression.Lambda(Expression.Convert(Expression.Property(parameterExpression, propertyName), typeof(object)), parameterExpression).Compile();
            _getPropertyValueFuncs[ordinal] = func;

            ordinal++;
        }
    }

    public override object this[int ordinal] 
    {
        get
        {
            return GetValue(ordinal);
        }
    }

    public override object this[string name]
    {
        get
        {
            return GetValue(GetOrdinal(name));
        }
    }

    public override int Depth => 1;

    public override int FieldCount => _ordinalToPropertyName.Count;

    public override bool HasRows => true;

    public override bool IsClosed
    {
        get
        {
            return _iterator != null;
        }
    }

    public override int RecordsAffected
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    public override bool GetBoolean(int ordinal)
    {
        return (bool)GetValue(ordinal);
    }

    public override byte GetByte(int ordinal)
    {
        return (byte)GetValue(ordinal);
    }

    public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
    {
        throw new NotImplementedException();
    }

    public override char GetChar(int ordinal)
    {
        return (char)GetValue(ordinal);
    }

    public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
    {
        throw new NotImplementedException();
    }

    public override string GetDataTypeName(int ordinal)
    {
        throw new NotImplementedException();
    }

    public override DateTime GetDateTime(int ordinal)
    {
        return (DateTime)GetValue(ordinal);
    }

    public override decimal GetDecimal(int ordinal)
    {
        return (decimal)GetValue(ordinal);
    }

    public override double GetDouble(int ordinal)
    {
        return (double)GetValue(ordinal);
    }

    public override IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }

    public override Type GetFieldType(int ordinal)
    {
        var value = GetValue(ordinal);
        if (value == null)
            return typeof(object);

        return value.GetType();
    }

    public override float GetFloat(int ordinal)
    {
        return (float)GetValue(ordinal);
    }

    public override Guid GetGuid(int ordinal)
    {
        return (Guid)GetValue(ordinal);
    }

    public override short GetInt16(int ordinal)
    {
        return (short)GetValue(ordinal);
    }

    public override int GetInt32(int ordinal)
    {
        return (int)GetValue(ordinal);
    }

    public override long GetInt64(int ordinal)
    {
        return (long)GetValue(ordinal);
    }

    public override string GetName(int ordinal)
    {
        string name;
        if (_ordinalToPropertyName.TryGetValue(ordinal, out name))
            return name;

        return null;
    }

    public override int GetOrdinal(string name)
    {
        int ordinal;
        if (_propertyNameToOrdinal.TryGetValue(name, out ordinal))
            return ordinal;

        return -1;
    }

    public override string GetString(int ordinal)
    {
        return (string)GetValue(ordinal);
    }

    public override object GetValue(int ordinal)
    {
        var func = _getPropertyValueFuncs[ordinal];
        return func(_iterator.Current);
    }

    public override int GetValues(object[] values)
    {
        int max = Math.Min(values.Length, FieldCount);
        for (var i = 0; i < max; i++)
        {
            values[i] = IsDBNull(i) ? DBNull.Value : GetValue(i);
        }

        return max;
    }

    public override bool IsDBNull(int ordinal)
    {
        return GetValue(ordinal) == null;
    }

    public override bool NextResult()
    {
        return false;
    }

    public override bool Read()
    {
        return _iterator.MoveNext();
    }
}

Dann können Sie diese Klasse verwenden:

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");
    string connectionString = "Server=(local);Database=Sample;Trusted_Connection=True;";

    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();

        using (var command = connection.CreateCommand())
        {
            command.CommandType = System.Data.CommandType.StoredProcedure;
            command.CommandText = "procMergePageView";

            var p1 = command.CreateParameter();
            command.Parameters.Add(p1);    
            p1.ParameterName = "@Display";
            p1.SqlDbType = System.Data.SqlDbType.Structured;
            var items = PageViewTableType.Generate(100);
            using (DbDataReader dr = new ObjectDataReader<PageViewTableType>(items))
            {
                p1.Value = dr;
                command.ExecuteNonQuery();
            }    
        }
    }
}

class PageViewTableType
{
    // Must match the name of the column of the TVP
    public long PageViewID { get; set; }

    // Generate dummy data
    public static IEnumerable<PageViewTableType> Generate(int count)
    {
        for (int i = 0; i < count; i++)
        {
            yield return new PageViewTableType { PageViewID = i };
        }
    }
}

Die SQL-Skripte:

CREATE TABLE dbo.PageView
(
    PageViewID BIGINT NOT NULL CONSTRAINT pkPageView PRIMARY KEY CLUSTERED,
    PageViewCount BIGINT NOT NULL
);
GO

CREATE TYPE dbo.PageViewTableType AS TABLE
(
    PageViewID BIGINT NOT NULL
);
GO

CREATE PROCEDURE dbo.procMergePageView
    @Display dbo.PageViewTableType READONLY
AS
BEGIN
    MERGE INTO dbo.PageView AS T
    USING @Display AS S
    ON T.PageViewID = S.PageViewID
    WHEN MATCHED THEN UPDATE SET T.PageViewCount = T.PageViewCount + 1
    WHEN NOT MATCHED THEN INSERT VALUES(S.PageViewID, 1);
END

Ich habe übrigens einen Blogbeitrag über den ObjectDataReader<T> geschrieben.

8
meziantou

Es gibt zwei Lösungen für dieses Problem. Einer verwendet DbDataReader, wie @meziantou in seiner Antwort vorschlägt, und war nett, eine generische Methode bereitzustellen, die einen IEnumerable<T> in eine DbDataReader konvertiert.

Die andere Lösung, die ich gefunden habe, war SqlDataRecord, also schreibe ich sie hier auf (benutze was immer du für deine Bedürfnisse hältst):

SQL Server-Tabelle:

CREATE TABLE [dbo].[Users](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [nvarchar](50) NULL,
    [LastNAme] [nvarchar](50) NULL,
 CONSTRAINT [PK_USers] PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Benutzerdefinierter Tabellentyp:

CREATE TYPE [dbo].[TblUser] AS TABLE(
    [FirstName] [nvarchar](50) NULL,
    [LastNAme] [nvarchar](50) NULL
)

.NET Core-Code:

var db = new SqlConnection("Server=localhost; Database=Test; User Id=test; Password=123456;");

List<SqlDataRecord> users = new List<SqlDataRecord>();

SqlMetaData mDataFirstName = new SqlMetaData("FirstName", SqlDbType.NVarChar, 50);
SqlMetaData mDataLastName = new SqlMetaData("LastName", SqlDbType.NVarChar, 50);

SqlDataRecord user1 = new SqlDataRecord(new []{ mDataFirstName, mDataLastName });
user1.SetString(0, "Ophir");
user1.SetString(1, "Oren");
users.Add(user1);

SqlParameter param = new SqlParameter("@Users", SqlDbType.Structured)
{
    TypeName = "TblUser",
    Value = users
};

Dictionary<string, object> values = new Dictionary<string, object>();
values.Add("@Users", param);


db.Open();
using (var command = db.CreateCommand())
{
    command.CommandType = System.Data.CommandType.StoredProcedure;
    command.CommandText = "stp_Users_Insert";

    var p1 = command.CreateParameter();
    command.Parameters.Add(p1);
    p1.ParameterName = "@Users";
    p1.SqlDbType = System.Data.SqlDbType.Structured;
    p1.Value = users;
    command.ExecuteNonQuery();
}
1
developer82

@meziantou. Ich liebe deine Antwort, aber bei deiner Implementierung gibt es einen brechenden Fehler. Das erste Problem war, dass MoveNext im Konstruktor aufgerufen wurde, was dazu führen würde, dass der Leser immer den ersten Wert überspringt. Nachdem ich das entfernt hatte, entdeckte ich, warum das überhaupt gemacht wurde. Ich habe GetFieldType geändert, um die Typinformationen zu verwenden, anstatt den Typ aus dem Wert zu lesen, wodurch das Problem behoben wurde. Wieder eine wirklich ausgezeichnete Antwort von Ihnen. Vielen Dank für das Posten. Hier ist meine überarbeitete Version von ObjectDataReader.

    public class ObjectDataReader<T> : DbDataReader
    {   
        private bool _iteratorOwned;
        private IEnumerator<T> _iterator;
        private IDictionary<string, int> _propertyNameToOrdinal = new Dictionary<string, int>();
        private IDictionary<int, string> _ordinalToPropertyName = new Dictionary<int, string>();
        private PropertyInfoContainer[] _propertyInfos;

        class PropertyInfoContainer
        {
            public Func<T, object> EvaluatePropertyFunction { get; set; }
            public Type PropertyType { get; set; }
            public string PropertyName { get; set; }
            public PropertyInfoContainer(string propertyName
                , Type propertyType
                , Func<T, object> evaluatePropertyFunction)
            {
                this.PropertyName = propertyName;
                this.PropertyType = propertyType;
                this.EvaluatePropertyFunction = evaluatePropertyFunction;
            }
        }

        public ObjectDataReader(IEnumerable<T> enumerable)
        {
            if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));

            _iteratorOwned = true;
            _iterator = enumerable.GetEnumerator();
            //_iterator.MoveNext();
            Initialize();
        }

        public ObjectDataReader(IEnumerator<T> iterator)
        {
            if (iterator == null) throw new ArgumentNullException(nameof(iterator));

            _iterator = iterator;
            Initialize();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && _iteratorOwned)
            {
                if (_iterator != null)
                    _iterator.Dispose();
            }

            base.Dispose(disposing);
        }

        private void Initialize()
        {
            int ordinal = 0;
            var properties = typeof(T).GetProperties();
            _propertyInfos = new PropertyInfoContainer[properties.Length];
            foreach (var property in properties)
            {
                string propertyName = property.Name;
                _propertyNameToOrdinal.Add(propertyName, ordinal);
                _ordinalToPropertyName.Add(ordinal, propertyName);

                var parameterExpression = Expression.Parameter(typeof(T), "x");
                var func = (Func<T, object>)Expression.Lambda(Expression.Convert(Expression.Property(parameterExpression, propertyName), typeof(object)), parameterExpression).Compile();
                _propertyInfos[ordinal] = new PropertyInfoContainer(property.Name
                    , property.PropertyType
                    , func);

                ordinal++;
            }
        }

        public override object this[int ordinal]
        {
            get
            {
                return GetValue(ordinal);
            }
        }

        public override object this[string name]
        {
            get
            {
                return GetValue(GetOrdinal(name));
            }
        }

        public override int Depth => 1;

        public override int FieldCount => _ordinalToPropertyName.Count;

        public override bool HasRows => true;

        public override bool IsClosed
        {
            get
            {
                return _iterator != null;
            }
        }

        public override int RecordsAffected
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override bool GetBoolean(int ordinal)
        {
            return (bool)GetValue(ordinal);
        }

        public override byte GetByte(int ordinal)
        {
            return (byte)GetValue(ordinal);
        }

        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
        {
            throw new NotImplementedException();
        }

        public override char GetChar(int ordinal)
        {
            return (char)GetValue(ordinal);
        }

        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
        {
            throw new NotImplementedException();
        }

        public override string GetDataTypeName(int ordinal)
        {
            throw new NotImplementedException();
        }

        public override DateTime GetDateTime(int ordinal)
        {
            return (DateTime)GetValue(ordinal);
        }

        public override decimal GetDecimal(int ordinal)
        {
            return (decimal)GetValue(ordinal);
        }

        public override double GetDouble(int ordinal)
        {
            return (double)GetValue(ordinal);
        }

        public override IEnumerator GetEnumerator()
        {
            throw new NotImplementedException();
        }

        public override Type GetFieldType(int ordinal)
        {    
            // cannot handle nullable types, so get underlying type
            var propertyType =
                                Nullable.GetUnderlyingType(_propertyInfos[ordinal].PropertyType) ?? _propertyInfos[ordinal].PropertyType;

            return propertyType;
        }

        public override float GetFloat(int ordinal)
        {
            return (float)GetValue(ordinal);
        }

        public override Guid GetGuid(int ordinal)
        {
            return (Guid)GetValue(ordinal);
        }

        public override short GetInt16(int ordinal)
        {
            return (short)GetValue(ordinal);
        }

        public override int GetInt32(int ordinal)
        {
            return (int)GetValue(ordinal);
        }

        public override long GetInt64(int ordinal)
        {
            return (long)GetValue(ordinal);
        }

        public override string GetName(int ordinal)
        {
            string name;
            if (_ordinalToPropertyName.TryGetValue(ordinal, out name))
                return name;

            return null;
        }

        public override int GetOrdinal(string name)
        {
            int ordinal;
            if (_propertyNameToOrdinal.TryGetValue(name, out ordinal))
                return ordinal;

            return -1;
        }

        public override string GetString(int ordinal)
        {
            return (string)GetValue(ordinal);
        }

        public override object GetValue(int ordinal)
        {
            var func = _propertyInfos[ordinal].EvaluatePropertyFunction;
            return func(_iterator.Current);
        }

        public override int GetValues(object[] values)
        {
            int max = Math.Min(values.Length, FieldCount);
            for (var i = 0; i < max; i++)
            {
                values[i] = IsDBNull(i) ? DBNull.Value : GetValue(i);
            }

            return max;
        }

        public override bool IsDBNull(int ordinal)
        {
            return GetValue(ordinal) == null;
        }

        public override bool NextResult()
        {
            return false;
        }

        public override bool Read()
        {
            return _iterator.MoveNext();
        }
    }
1
Eric

Ich hatte das gleiche Problem, dass Sie keine DataTable erstellen können und diese daher einfach in ein Blatt kopieren können.

Das Fehlen der DataTable-Unterstützung in Core zwingt Sie dazu, stark typisierte Objekte zu erstellen und diese dann durchzugehen und der Ausgabe von EPPlus zuzuordnen.

Ein sehr einfaches Beispiel ist also:

// Get your data directly from EF,
// or from whatever other source into a list,
// or Enumerable of the type
List<MyEntity> data = _whateverService.GetData();

using (ExcelPackage pck = new ExcelPackage())
{
   // Create a new sheet
   var newSheet = pck.Workbook.Worksheets.Add("Sheet 1");
   // Set the header:
   newSheet.Cells["A1"].Value = "Column 1 - Erm ID?";
   newSheet.Cells["B1"].Value = "Column 2 - Some data";
   newSheet.Cells["C1"].Value = "Column 3 - Other data";

  row = 2;
  foreach (var datarow in data)
  {
      // Set the data:
      newSheet.Cells["A" + row].Value = datarow.Id;
      newSheet.Cells["B" + row].Value = datarow.Column2;
      newSheet.Cells["C" + row].Value = datarow.Cilumn3;
      row++;
  }
}

Sie nehmen also eine zahllose Quelle für ein stark typisiertes Objekt, das Sie direkt von einer EF-Abfrage oder einem Ansichtsmodell oder etwas anderem ausführen können, und dann durchlaufen, um es abzubilden.

Ich habe dies verwendet und die Leistung erscheint - für einen Endbenutzer - auf dem Niveau der DataTable-Methode. Ich habe die Quelle nicht untersucht, aber es würde mich nicht überraschen, wenn die DataTable-Methode intern dasselbe macht und jede Zeile durchläuft.

Sie könnten eine Erweiterungsmethode erstellen, um Generics zum Übergeben in der Objektliste zu verwenden, und Reflektion verwenden, um sie richtig zuzuordnen.

Bearbeiten zum Hinzufügen:

In .NET Core scheint es, aus dem GitHub-Ausgaben-Tracker heraus, dass die DataTable-Unterstützung in der Prioritätenliste ziemlich niedrig ist und nicht zu erwarten ist. Ich denke, es ist auch ein philosophischer Punkt, da das Konzept im Allgemeinen versucht, stark typisierte Objekte zu verwenden. Früher war es so, dass Sie eine SQL-Abfrage in eine DataTable ausführen und damit ausführen konnten. Nun sollten Sie diese Abfrage in einem Model ausführen, das entweder direkt einer Tabelle mit Entity Framework über eine DbSet oder mit ModelBinding und der Weitergabe zugeordnet ist einen Typ in die Abfrage.

Sie haben dann einen IQueryable<T>, der als Ihr stark typisierter Ersatz für DataTables dient. Um diesem Ansatz gerecht zu werden, ist dies in 99% der Fälle ein gültiger und besserer Ansatz. Es wird jedoch immer Zeiten geben, in denen der Mangel an DataTables Probleme verursacht und umgangen werden muss!

Weitere Bearbeitung

In ADO.NET können Sie eine datareader in eine stark typisierte Liste von Objekten konvertieren: Wie kann ich DataReader leicht in Liste <T> konvertieren? um nur ein Beispiel zu nennen. Mit dieser Liste können Sie von dort aus Ihre Zuordnung vornehmen.

Wenn Sie ASP.NET Core als Ziel ASP.NET Core framework verwenden möchten/müssen, müssen Sie dies tun. Wenn Sie mit einem Core-Projekt auf Net 4.5 zielen können, können Sie System.Data und DataTables verwenden. Der einzige Nachteil ist, dass Sie Windows-Server und IIS verwenden müssen.

Benötigen Sie wirklich das vollständige .Net Core-Framework? Müssen Sie auf Linux hosten? Wenn nein, und Sie wirklich DataTables benötigen, zielen Sie einfach auf das ältere Framework.

1
RemarkLima

Entwickler82,

Ich bin in der gleichen Situation, in der ich .net Core verwenden möchte, aber die Nichtverfügbarkeit von datatable ist ein Datensatz eine Schande. Da Sie auf einen Beitrag verweisen, der eine Liste verwendet, dachte ich, dass es das Ziel ist, die C # -Liste auf die sauberste Weise in die Datenbank zu bringen. Wenn dies das Ziel ist, kann dies helfen. 

Ich habe Dapper here in verschiedenen Projekten verwendet. Es wird int .netcore unterstützt. Im Folgenden finden Sie eine kleine Konsolenanwendung, die eine aufgefüllte c # -Liste verwendet, diese in die Datenbank einfügt und dann eine Auswahl für diese Tabelle ausgibt, um die Ergebnisse in die Konsole zu schreiben.

using System;
using System.Data;
using Dapper;
using System.Data.Common;
using System.Data.SqlClient;
using System.Collections.Generic;

namespace TestConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            List<DataItemDTO> dataItems = GetDataItems();

            var _selectSql = @"SELECT CustomerId, Name, BalanceDue from [dbo].[CustomerAccount]";

            var _insertSql = @"INSERT INTO [dbo].[CustomerAccount]
                                       ([CustomerId]
                                       ,[Name]
                                       ,[BalanceDue])
                                 VALUES
                                       (@CustomerId
                                       ,@Name
                                       ,@BalanceDue)";



            using (IDbConnection cn = new SqlConnection(@"Server=localhost\xxxxxxx;Database=xxxxdb;Trusted_Connection=True;"))
            {
                var rows = cn.Execute(_insertSql, dataItems,null,null,null );

                dataItems.Clear();

                var results = cn.Query<DataItemDTO>(_selectSql);

                foreach (var item in results)
                {
                    Console.WriteLine("CustomerId: {0} Name {1} BalanceDue {2}", item.CustomerId.ToString(), item.Name, item.BalanceDue.ToString());
                }


            }

            Console.WriteLine("Press any Key");
            Console.ReadKey();

        }

        private static List<DataItemDTO> GetDataItems()
        {
            List<DataItemDTO> items = new List<DataItemDTO>();

            items.Add(new DataItemDTO() { CustomerId = 1, Name = "abc1", BalanceDue = 11.58 });
            items.Add(new DataItemDTO() { CustomerId = 2, Name = "abc2", BalanceDue = 21.57 });
            items.Add(new DataItemDTO() { CustomerId = 3, Name = "abc3", BalanceDue = 31.56 });
            items.Add(new DataItemDTO() { CustomerId = 4, Name = "abc4", BalanceDue = 41.55 });
            items.Add(new DataItemDTO() { CustomerId = 5, Name = "abc5", BalanceDue = 51.54 });
            items.Add(new DataItemDTO() { CustomerId = 6, Name = "abc6", BalanceDue = 61.53 });
            items.Add(new DataItemDTO() { CustomerId = 7, Name = "abc7", BalanceDue = 71.52 });
            items.Add(new DataItemDTO() { CustomerId = 8, Name = "abc8", BalanceDue = 81.51 });

            return items;
        }
    }
}

Ich hoffe, dieses Codebeispiel hilft.

danke dir.

0
Ed Mendez