Laden...

ObservableCollection für mehrere Typen: Wie kann ich den Typ festlegen?

Erstellt von Kriz vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.457 Views
K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 5 Jahren
ObservableCollection für mehrere Typen: Wie kann ich den Typ festlegen?

Moin zusammen,

folgender Sachverhalt:
in meinem Projekt habe ich mehrere Klassen, beispielsweise Mitarbeiter, Schichten, oder Feiertage.
Nun habe ich eine Datenbank in der diese Mitarbeiter, Schichten und Feiertage abgelegt sind.
Zum Abrufen der Daten habe ich eine Methode geschrieben die eine ObservableCollection<object> zurück gibt, damit ich nicht für jede Klasse eine eigene Abfrage schreiben muss die dann den jeweiligen Typ wieder gibt. Ich hoffe ich konnte es verständlich erklären
Nun zu meiner Frage: Gibt es eine Möglichkeit diese ObservableCollection<object> in eine ObservableCollection<Mitarbeiter> zu konvertieren?


public static ObservableCollection<object> GetListOf(string ObjectName)
        {
                ObservableCollection<object> tmpList = new ObservableCollection<object>();
                SQLiteConnection connection = new SQLiteConnection() { ConnectionString = Const.DatabaseConnectionString() };

                SQLiteCommand command = new SQLiteCommand(connection);
                command.CommandText = "SELECT * FROM " + ObjectName + " WHERE IsDeleted LIKE 'FALSE' AND ID != '0' ORDER BY Name";
                connection.Open();

                SQLiteDataReader reader = command.ExecuteReader();

                switch (ObjectName)
                {
                    case "Employee":
                        List<Employee> newList = new List<Employee>();
                        while (reader.Read())
                        {
                            Employee tmpEmployee = new Employee()
                            {
                                //Hier werden dann die verschiedenen Eigenschaften befüllt
                            };
                            newList.Add(tmpEmployee);
                        }
                        newList.Sort();
                        tmpList = new ObservableCollection<object>(newList);
                        connection.Close();
                        connection.Dispose();
                        command.Dispose();
                        
                        return tmpList;

                      case "Holiday":
                        while (reader.Read())
                        {
                            Common.Holiday tmpHoliday = new Common.Holiday()
                            {
                                //Hier werden dann die verschiedenen Eigenschaften befüllt
                            };
                            tmpList.Add(tmpHoliday);
                        }
                        connection.Close();
                        command.Dispose();
                        connection.Dispose();
                        return tmpList;

                       //Und noch einige weitere case-Bedingungen...

        }

Wenn ich dann die Liste meiner Mitarbeiter brauche:


ObservableCollection<object> NeueListe = Datenbank.GetListOf("Employee");

Ich hoffe ich konnte rüberbringen was ich meine...

Danke im Vorraus!
Kriz

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo Kriz,

eigentlich ist es kein Problem ein Repository für die jeweilige Klasse zu schreiben, die sich dann auch nur um die jeweilige Klasse kümmert. Dann wäre dein Problem schon behoben. Dann fallen auch deine ganzen if/else's weg. Deine Methode wächst und wächst und wird unwartbar in der Zukunft (wenn sie es nicht jetzt schon ist) 😃

Weiter schau dir mal [Artikelserie] SQL: Parameter von Befehlen an.

Gruss

Coffeebean

1.029 Beiträge seit 2010
vor 5 Jahren

Hi,

entschuldige - aber du benutzt doch schon Generics - wieso dann nicht richtig ?!

Es ist doch kein Problem eine generische Klasse zu schreiben, welche automatisch die richtige Abfrage je nach Objekttyp sendet (als Constraint könntest du hier sogar die Mitarbeiter-Klasse angeben) und das dann sogar typsicher zurück liefert...

LG

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo zusammen,

@Taipi88: Das dachte ich auch, aber er befüllt ja noch pro Klasse (unterschiedliche?) Properties. Bei einer generischen Lösung braucht er auch einen spezifischen Mapper dann irgendwo, oder übersehe ich was?

Gruss

Coffeebean

1.029 Beiträge seit 2010
vor 5 Jahren

@Coffeebean:
Ja das ist mir schon klar - die Abfrage selbst kann man allerdings dynamisch bauen - und wenn er dann noch Dapper oder einen anderen Mapper verwendet braucht er sich nicht mehr anstrengen um die Klassen abzufüllen... Alles besser wie diese MagicStrings zur Datenbankabfrage...

2.207 Beiträge seit 2011
vor 5 Jahren

Hallo Taipi88,

absolut, ja.

Wollte nur klarstellen, dass die generische Methode nur der eine Teil ist, das Mapping dann der andere. Aber ja, automatisiert sollte das auch kein Problem sein.

Gruss

Coffeebean

W
955 Beiträge seit 2010
vor 5 Jahren

Ich würde eher sagen dass das der richtige Zeitpunkt ist sich mit einem ORM zu beschäftigen.

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 5 Jahren

@coffeebeans und Taipi88,

Anfangs hatte ich auch pro Klasse eine eigene Datenbankabfrage bis mir dann aufgefallen ist, dass sich viele Sachen wiederholt haben und mir eine Klasse für Abfragen sinnvoller vorkam. Aber tatsächlich wird es langsam unübersichtlich...

Was hat es mit diesem Dapper bzw Mapper auf sich?

entschuldige - aber du benutzt doch schon Generics - wieso dann nicht richtig ?!

Es ist doch kein Problem eine generische Klasse zu schreiben, welche automatisch die richtige Abfrage je nach Objekttyp sendet (als Constraint könntest du hier sogar die Mitarbeiter-Klasse angeben) und das dann sogar typsicher zurück liefert...

Da tuh ich mich gerade etwas schwer dran... wie genau meinst du das? Also ich möchte jetzt keinen fertigen Code, sondern eher einen Verstehensanstoss... und wie und wo kommt dieser Mapper ins Spiel?

@witte
Wenn ich es richtig verstanden habe, dann bedeutet ORM schlicht dass pro Klasse eine Tabelle vorliegt wo für jede Property eine Spalte existiert. Und das ist schon so. Oder hab ich da was falsch verstanden?

Vielen Dank!
Kriz

1.029 Beiträge seit 2010
vor 5 Jahren

Hi,

naja - ORM's und Dapper haben teils ähnliche Aufgaben - das Thema ist:
Man schickt einem SQL-Server eine meist string-basierte Anfrage - und bekommt Daten dafür zurück. Das Problem: Die Daten die dort kommen sind nicht in dem "Format" wie man das gerne hätte. (Instanzen eigener Datenklassen)

1) Zu Dapper und ORM's
Dapper bietet z.B. die Möglichkeit, dass diese Daten automatisch in entsprechende Objekte abgefüllt werden - es mappt also die Daten des jeweiligen SQL-Servers auf deine eigenen Klassen/Objekte, was die Arbeit stark vereinfacht.
ORM's machen das selbe - bieten allerdings i.d.R. auch mehr oder minder direkten Datenbankzugriff und automatische Erstellung von SQL-Statements an - und zwar so, dass man leicht vergisst, dass man es hier mit SQL zu tun hat.
Hinweis: ORM's kosten in aller Regel ein bisschen Performance - und können dich teilweise einschränken in deinen Möglichkeiten der Datenbankkommunikation

2) Generics, wie das gemeint ist
Naja - was ich kritisiere an deinem Code ist:
Du verwendest eine generische Klasse, obwohl dein Rückgabetyp lediglich "object" ist - das zerstört den Sinn von generics, da diese ja eigentlich (auch) Typsicherheit geben sollten - von der Definition sollte deine Klasse eher folgendermaßen aussehen:


public class Repository<T> where T : class
    {
        public ObservableCollection<T> GetAll()
        {
            return new ObservableCollection<T>();
        }
    }

Dann kann man das nämlich folgendermaßen benutzen und bekommt nicht nur den Rückgabewert "object":


class Program
    {
        static void Main(string[] args)
        {
            var repository = new Repository<Customer>();
            var customers = repository.GetAll();
        }
    }

3) Wie könnte eine "richtige" Implementierung aussehen
Das kann man hier nur anreißen - aber das oben gezeigte Repository könnte z.B. den Namen der Tabelle aus dem Typparater "T" auslesen (in der Realität schreibt man hierfür eher Attribute oder bietet anderweitig eine Konfigurationsmöglichkeit) - im einfachsten Fall würde es dann so aussehen (ohne Mapping bislang):


public class Repository<T> where T : class
    {
        public ObservableCollection<T> GetAll()
        {
            var sql = $"SELECT * FROM {typeof(T).Name}";
            // fetch data from sql-server
            // map data to the desired object --> ObservableCollection<T>
            return new ObservableCollection<T>();
        }
    }

Du siehst - da gibt es viele Möglichkeiten... Aber das wird für guten Code auch noch nicht reichen - für eine gescheite Datenschicht gibt es einige möglichen Konstrukte (Patterns) wie man das ordentlich aufbaut - ich verwende z.B. "UnitOfWorkPattern" und biete i.d.R. Repositories an. Ein solches Repository sieht im einfachsten Fall bei mir z.B. folgendermaßen aus:


namespace FluiTec.AppFx.Data.Dapper
{
    /// <summary>	A dapper read only repository. </summary>
    /// <typeparam name="TEntity">	Type of the entity. </typeparam>
    /// <typeparam name="TKey">   	Type of the key. </typeparam>
    public abstract class DapperReadOnlyRepository<TEntity, TKey> : IReadOnlyDataRepository<TEntity, TKey>
        where TEntity : class, IEntity<TKey>, new()
    {
        #region Constructors

        /// <summary>   Constructor. </summary>
        /// <param name="unitOfWork">   The unit of work. </param>
        protected DapperReadOnlyRepository(IUnitOfWork unitOfWork)
        {
            UnitOfWork = unitOfWork as DapperUnitOfWork;
            if (UnitOfWork == null)
                throw new ArgumentException(
                    $"{nameof(unitOfWork)} was either null or does not implement {nameof(DapperUnitOfWork)}!");

            SqlBuilder = UnitOfWork.Connection.GetBuilder();
            TableName = GetTableName(typeof(TEntity));
            EntityType = typeof(TEntity);
        }

        #endregion

        #region Methods

        /// <summary>	Gets table name. </summary>
        /// <returns>	The table name. </returns>
        protected string GetTableName(Type t)
        {
            return SqlBuilder.Adapter.RenderTableName(t);
        }

        #endregion

        #region Properties

        /// <summary>   Gets the name of the table. </summary>
        /// <value> The name of the table. </value>
        public virtual string TableName { get; }

        /// <summary>   Gets the unit of work. </summary>
        /// <value> The unit of work. </value>
        public DapperUnitOfWork UnitOfWork { get; }

        /// <summary>	Gets the SQL builder. </summary>
        /// <value>	The SQL builder. </value>
        protected SqlBuilder SqlBuilder { get; }

        /// <summary>	Gets the type of the entity. </summary>
        /// <value>	The type of the entity. </value>
        protected Type EntityType { get; }

        #endregion

        #region IReadOnlyRepository

        /// <summary>	Gets a t entity using the given identifier. </summary>
        /// <param name="id">	The Identifier to get. </param>
        /// <returns>	A TEntity. </returns>
        public virtual TEntity Get(TKey id)
        {
            return UnitOfWork.Connection.Get<TEntity>(id, UnitOfWork.Transaction);
        }

        /// <summary>	Gets all items in this collection. </summary>
        /// <returns>
        ///     An enumerator that allows foreach to be used to process all items in this collection.
        /// </returns>
        public virtual IEnumerable<TEntity> GetAll()
        {
            return UnitOfWork.Connection.GetAll<TEntity>(UnitOfWork.Transaction);
        }

        /// <summary>Gets the count.</summary>
        /// <returns>An int.</returns>
        public int Count()
        {
            var command = $"SELECT COUNT({SqlBuilder.Adapter.RenderPropertyName(nameof(IEntity<int>.Id))}) FROM {TableName}";
            return UnitOfWork.Connection.ExecuteScalar<int>(command, null, UnitOfWork.Transaction);
        }

        #endregion
    }
}

_Dieser Codebrocken ist nur ein Beispiel - das Thema dabei ist - der hier vorliegende Code generiert auf Basis des Typparameters (TEntity) SQL-Statements und benutzt Dapper um automatisch die Daten die vom SQL-Server kommen zu einer Instanz von TEntity zu mappen, wobei TEntity lediglich die Schnittstelle IEntity<TKey> impllementieren muss. In anderen Worten - solang die Datenklasse IEntity implementiert

  • funktioniert das Repository für die Klasse ohne dass jemand am Code was ändern muss - also für unzählige Objekte --> für sowas setzt man Generics ein_

Ich vermute ehrlich gesagt mal, dass es dir aktuell noch schwer fallen wird etwas ähnliches zu bauen - ist ein wenig aufwändig - dementsprechend könntest du dir mal verschiedene ORM's anschauen - ich denke das einfachste wäre das EntityFramework bzw. EntityFramework Core. (Einfach mal googeln)

LG

4.931 Beiträge seit 2008
vor 5 Jahren

Ein OR-Mapper (ORM), wie z.B. (das leichtgewichtige) Dapper sorgt dafür, daß man nicht mehr händisch die Eigenschaften aus den Spalten kopieren muß.
Für deinen Code also in etwa:


var employees = connection.Query<Employee>("SELECT IsDeleted LIKE 'FALSE' AND ID != '0' ORDER BY Name", null);
// bzw. wohl eher
var employees = connection.Query<Employee>("SELECT IsDeleted = FALSE AND ID != 0 ORDER BY Name", null);

Und was Taipi88 mit generisch meint ist:


// Aufruf:
ObservableCollection<Employee> NeueListe = Datenbank.GetListOf<Employee>();

// Definition:
public static ObservableCollection<T> GetListOf<T>()
{
  // benutze T als Datentyp
}

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 5 Jahren

Ist schon etwas länger her, aber ich versuch es hier trotzdem nochmal...

Wenn ich nun eine Methode mit einer generischen ObservabeleCollection als Rückgabe deklariere, dann bekomme ich als Fehlermeldung: > Fehlermeldung:

Konvertierung von Emloyee in T nicht möglich. Wie muss ich es richtig angehen?

mein Code (Beispiel):


public static ObservableCollection<T> getList<T>()
        {
            ObservableCollection<T> result = new ObservableCollection<T>();

            Common.Employee employee = new Common.Employee(); //Hier die Datenbankabfrage
            result.Add(employee);

            return result;
        }

16.806 Beiträge seit 2008
vor 5 Jahren

Weiß zwar nicht, was Du da vor hast; aber das kann so nicht funkionieren.

Du darfst in einer generischen Methode nicht mit fixen Typen (hier Employee) arbeiten.
Sondern eben nur mit dem generischen Typ (hier T, besserer Name wäre TEntity).

Es gibt hier nichts zu observen.

public static IList<TEntity> GetList<TEntity>()
        {
            IList<TEntity> entities = UnitOfWork.Connection.GetAll<TEntity>... //Hier die Datenbankabfrage
    
            return entities;
        }

PS:
ObservableCollection hat in der Datenbankkommunikation nichts zu suchen.
Im DAL arbeitet man i.d.R. mit IList oder ICollection.

PPS: es heisst GetList, nicht getList.
Wir sind hier in C# - nicht Java -> [Artikel] C#: Richtlinien für die Namensvergabe 😉

K
Kriz Themenstarter:in
141 Beiträge seit 2017
vor 5 Jahren

Ich habe eine Klasse die Datenbankabfragen durchführt und bei Bedarf eine Liste bzw eine ObservableCollection verschiedenster Klassen zurück gibt. Also mal eine Liste mit emploees, mal mit contracts, mal mit schedules, usw... bis jetzt habe ich das mit object umgesetzt, also es wurde immer eine ObservableCollection<object> zurück gegeben. Nun komm eich aber an einen Punkt wo es einfacher wäre, wenn ich die ObservableCollection schon mit dem passenden Datentyp zurück bekommen würde.

16.806 Beiträge seit 2008
vor 5 Jahren

Ich habe eine Klasse die Datenbankabfragen durchführt und bei Bedarf eine Liste bzw eine ObservableCollection verschiedenster Klassen zurück gibt.

Das ist konzeptionell ungünstig.
ObservableCollection ist ein Werkzeug, das hauptsächlich in der UI Verwendung findet - manchmal auch im Application State (zB mit Reactive Extensions); aber nicht in der Datenbank-Kommunikation.
Da macht es einfach auch technologisch keinen Sinn.

[Artikel] Drei-Schichten-Architektur

bis jetzt habe ich das mit object umgesetzt

Sobald Du object für sowas verwendest, müssen alle Alarmglocken läuten, dass hier was am Konzept nicht stimmen kann.
In .NET arbeitet man mit konkreten Typen; wir haben hier eine typisierte Sprache! 😉

Es ist also nicht nur einfacher mit konkreten Typen zu arbeiten, sondern auch der einzig korrekte Weg 😃
Ansonsten hast Du mit Taipi88's Code ja schon eine entsprechende Basis (und Sample).