Laden...

Erstes C#, MVVM und SQLite Projekt

Erstellt von Moritz83 vor 4 Jahren Letzter Beitrag vor 4 Jahren 10.695 Views
Thema geschlossen
M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren
Erstes C#, MVVM und SQLite Projekt

Moin,

wie in meinen beiden anderen Threads bereits angesprochen beschäftige ich mich im Moment mit C#, MVVM, WPF und SQLite (Ich selber habe nur ein wenig VBA (Excel) und Access Erfahrung, sprich dies ist mein erstes Projekt mit C# überhaupt)

Zu meinem "Projekt":
Als Backend dient mir eine SQLite Datei mit 2 Tabellen (Mitarbeiter und Team) und ich möchte jedem Mitarbeiter (ID, Vorname, Nachname) ein Team zuordnen. Dieses Team soll mir im späteren Verlauf eine mandantenfähige Lösung dienlich sein (dazu weiter unten mehr). Das Anlegen ieines neuen Mitarbeiters oder Teams kann via Menu oben vorgenommen warden, das Löschen funktioniert via "Delete". De Änderungen warden allesamt in die SQLite Datei zurück gespielt.
Bitte unter "settings.settings" den Link zur Test.db anpassen bevor das Projekt debuggt wird 😉

Was ich mir wünsche:
Wie bereits gesagt stehe ich ganz am Anfang mit C# und würde mich freuen wenn ihr euch mein kleines Projekt ansehen könntet. Denke da ist eine ganze Menge an Verbesserungspotential vorhanden. Würde mir natürlich auch wünschen das Verbesserungsvorschläge ev. direct an meinem Code aufgezeigt würden.

Zum weiteren Verlauf:
Ich möchte dieses "Miniprojekt" nutzen um eine bestehende Accesslösung abzulösen. Es wird noch 2-3 weitere solche Projekte geben die dann schlussendlich in ein grosses Ganzes übernommen warden … davon bin ich aber 1.) noch weit weit entfernt und 2.) möchte ich so gut als mir möglich Best-Practice betreiben (klar, ein Profi würde das ganz anders angehen)

Link:
<Link entfernt>

Sonstiges:
An dieser Stelle schon mal vielen Dank an alle die meinen Post gelesen haben 😃

16.807 Beiträge seit 2008
vor 4 Jahren

Bitte stell Dein Projekt auf einer Plattform wie GitHub, GitLab oder Azure DevOps (Public Repository) zur Verfügung. Den Link hab ich entfernt.
Besten Dank!

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

au weia, hoffe ich habe das nun richtig gemacht mit GitHub … falls net bitte melden (musste mir erstmal n Account anlegen)

--> https://github.com/MoritzJuergensen/SQLite (Datenbank befindet sich im Database Ordner --- Pfad muss wie oben angegeben angepasst warden)

301 Beiträge seit 2009
vor 4 Jahren

Ich hab nur mal ganz grob reingeschaut. Ein nächster logischer Schritt wäre ggf. deine Datenabfragen aus deinem Model rauszuholen und somit zu entkoppeln.

Desweiteren ist es empfehlenswert ein Lightweight ORM wie z.B. "Dapper" ( Hier gibt es auch Extensions für SQLite ) einzusetzen damit du nicht selbst den DataReader nutzen musst um die Daten aus der DB in deine Anwendung zu holen.

5.657 Beiträge seit 2006
vor 4 Jahren

Hier ein paar Tips:

[Artikelserie] SQL: Parameter von Befehlen
[Artikel] Drei-Schichten-Architektur
[Artikel] MVVM und DataBinding
[Artikel] C#: Richtlinien für die Namensvergabe

Ansonsten macht sowas hier wenig Sinn:


public void MyMethod(object parameter)
{
  if ((string)paramter == "Team")
    CreateTeam();
  // etc.
}

Erstens sollte man schon den richtigen Datentyp verwenden (in dem Fall string statt object), und zweitens kann man auch direkt die CreateTeam-Methode aufrufen. Magic Strings sind aber auf keinen Fall zu empfehlen.

Weeks of programming can save you hours of planning

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

erstmal danke euch Beiden!

@KroaX
Muss ich mir anschauen wie das mit dem ORM funktioniert, lese das hier zum ersten Mal. Yep habe einfach mal "alles" ins ViewModel gepackt, funktioniert aber def. keine optimale Lösung. Hier dran muss ich defitiniv arbeiten

@MrSparkle
denke du sprichst von dieser Methode, oder?

        private void CreateNewRow(object param)
        {
            string Parameter = (string) param;
            if (Parameter == "Mitarbeiter")
            {
                OC_Mitarbeiter.Add(new Mitarbeiter(99999, "?", "?", 1));
            }
            else if (Parameter == "Team")
            {
                OC_Team.Add(new Team(99999, "?"));
            }
            else
            {
                MessageBox.Show("falscher Parameter");
            }
        }

Die Methode hängt ja am Menu, sollte ich die beiden "Create" Methoden als einzelne Methoden definieren? (Hatte mir das irgendwie über die Zeit unter VBA angewöhnt alles zu verschachteln wenn es ähnliche Sachen sind --- hier wäre in dem Fall eine "CreateTeam" und Eine "CreateMitarbeiter" Methode sinnvoll?)

Zu den Links, Nr. 1 und 4 sind toller Lesestoff aber für 2 und 3 wäre es echt net wenn du mir praktische Beispiele anhand meines Beispiels geben würdest. Denke das würde mir helfen Theorie und Praxis besser miteinander zu verknüpfen. (Natürlich nur wenns net all zu grosse Mühe macht 😃 )

16.807 Beiträge seit 2008
vor 4 Jahren

Im Prinzip brauchst Du hier die Basics von OOP.
Dann kannst Du einfach mit einer generischen Methode sowohl Team wie auch Mitarbeiter darstellen.

Was Du da mit object machst ist im Prinzip völlig untypisiertes Programmieren und daher im Ansatz schon nicht gut.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Habe nun 2 Dinge gemacht:

1.) Ich hab mir endlich n C# Buch (Schrödinger programmiert…) gekauft (und werde nach dem Abschluss dieses MiniProjekts nochmal von 0 anfangen mit dem Buch)
und
2.) Ich habe versucht den Teil mit den Mitarbeitern mit Dapper umzusetzen. Hier erstmal der Code dazu (komplett neu aufgebaut):

Employee.cs

namespace SQLiteDapper.Model
{
    public class Employee
    {
        public int ID_Mitarbeiter { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Team { get; set; }
    }
}

IEmployeeRepository.cs

using System.Collections.Generic;

namespace SQLiteDapper.Model
{
    public interface IEmployeeRepository
    {
        List<Employee> GetAll();
        Employee GetById(int id);
        bool Update(Employee employee);
    }
}

EmployeeRepository.cs

using Dapper;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Linq;

namespace SQLiteDapper.Model
{
    class EmployeeRepository : IEmployeeRepository
    {
        private SQLiteConnection db = new SQLiteConnection(Properties.Settings.Default.connString);
        // Get Employee record by Id
        public Employee GetById(int id)
        {
            return this.db.Query<Employee>("SELECT * FROM Mitarbeiter WHERE ID_Mitarbeiter=@Id", new { Id = id }).FirstOrDefault();
        }
        // Retreives the data from the table.
        public List<Employee> GetAll()
        {
            return this.db.Query<Employee>("SELECT * FROM Mitarbeiter").ToList();
        }
        // Update the employee record
        public bool Update(Employee employee)
        {
            string query = "UPDATE Mitarbeiter SET LastName = @LastName WHERE ID_Mitarbeiter = @ID_Mitarbeiter";
            var count = this.db.Execute(query, employee);
            return count > 0;
        }
    }
}

ViewModel.cs

using SQLiteDapper.Model;
using System.Collections.Generic;

namespace SQLiteDapper.ViewModels
{
    public partial class ViewModel
    {
        public IEnumerable<Employee> Employees { get; set; }
        public ViewModel ()
            {
            // Get data for View Datagrid
            IEmployeeRepository employeeRepository = new EmployeeRepository();
            Employees = employeeRepository.GetAll(); //per Binding ins XAML File
            Employee emp = employeeRepository.GetById(1);
            emp.LastName = "Blödsinn";
            employeeRepository.Update(emp);
        }
    }
}

So, es funktioniert soweitber nun stehe ich aber vor einem Problem das ich ohne Hilfe def. net sauber lösen kann:

Für eine Liste funktioniert onpropertychanged ja nicht. Lohnt es sich die Liste in eine ObservableCollection umzuwandeln (Dapper unterstützt soweit ich weiss OC nicht direkt) oder soll ich diese sehr statischen Daten einfach als "Liste" belassen? (Meinen Datagrid kann ich ja trotzdem ändern und dann per Button oder so den Update Prozess anstossen) --> Falls Ja, wie mache ich das am "Besten" habs versucht aber das klappt bei mir nicht 😦 )?

16.807 Beiträge seit 2008
vor 4 Jahren

Was mir auf die Schnelle auffällt:

  • Wenn Du List als Return hast, dann empfange auch List statt IEnumerable (Intermediate Materialization (C#))
  • Prinzipiell ist schon gut, dass Du Datenbanksettings nicht fix hast. Fortgeschritten dann via Dependency Injection
  • OOP: Dein Interface ist korrekt nach IEmployeeRepository benannt; man sollte aber der Implementierung die Technik anerkennen. Hier: EmployeeSqliteRepository
    Warum: Wenn du mehrere Implementierungen hast (Sqlite, Mssql, Postgres...) sind diese besser zu unterscheiden und einfacher zu überblicken.
  • Die Eigenschaft ID_Mitarbeiter kann ganz einfach nur "Id" heissen. Keine Notwendigkeit ein Suffix anzuhängen (und vor allem nich auf Deutsch 😃 )
  • Die Eigenschaft "Team" ist wohl eher die TeamId, daher auch besser so nennen.
  • Bei einem Update ist es üblich(er), dass Du die aktualisierte Entität zurück gibst, statt nur bool
  • Bei größeren Anwendungen würde man kein GetAll anbieten, weil die Datenbankgröße niemals vollständig im Memory platz haben könnte.
M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Danke für die fixe Antwort!

Das mit der Liste, ID_Mitarbeiter, TeamID und der OOP Benennung habe ich bereits umgesetzt, danke dafür!

Dependency Injection muss ich später mal nachgucken, denke das ist aber imho zweitrangig. Mein grösstes Problem ist das Thema "Update":

In einem Beispiel habe ich folgendes gefunden:

        private int UpdateStudent(Student student)    
        {    
            using (var connection = new SqlConnection(sqlConnectionString))    
            {    
                connection.Open();    
                var affectedRows = connection.Execute("Update Student set Name = @Name, Marks = @Marks Where Id = @Id", new { Id = studentId, Name = txtName.Text, Marks = txtMarks.Text });    
                connection.Close();    
                return affectedRows;    
            }    
        } 

(Originalpost

Sehe ich das richtig das ich jede Eigenschaft überspielen soll? (Beispiel: Ich ändere "LastName" und überspiele dann trotzdem "FirstName" und "TeamID" mit?) Könnte ja rechts das Datagrid einblenden und links dann 2 Textboxen und Eine Combobox mit den Daten füllen. Nach dem Aktualisieren könnte ich via Button den Update Prozess starten.

Oder könnte man nach dem Editieren eines Feldes im Datagrid den Updateprozes starten?

PS:
die grösste Tabelle hat im Moment (nach 2 Jahren) ca. 2000 Einträge, denke GetAll() dürfte da noch funktionieren, oder?

16.807 Beiträge seit 2008
vor 4 Jahren

Es macht keinen Sinn 2000 Einträge auf einmal zu laden; Du wirst diese niemals gleichzeitig anzeigen können.

Prinzipiell ja: bei SQL musst Du alle Eigenschaften manuell angeben, die Du aktualisieren willst.
Es gibt aber Dapper Extensions (Dapper.Contrib), die Methoden in vereinfachter Form zur Verfügung stellen, dass Du das SQL nicht manuell schreiben musst.
Projekt ist aber nicht sonderlich aktiv.

Bitte übernehm das Beispiel nicht in dieser Form 1:1.
Das Öffnen von Verbindungen gehört NICHT in eine Abfragemethode.

Wie Du die UI umsetzt ist völlig unabhängig von der Datenbank.
[Artikel] Drei-Schichten-Architektur
Prinzipiell verwendet man auch andere Klassen in der UI, Logik- und Datenschicht. Selten, dass in der realen Welt alle 3 Layer exakt die gleiche Prseäntation eines Modells haben.

Es gibt auch sehr moderne Ansätze, die überhaupt keine spezifischen Modelle mehr für UI und Logik kennen.
Hier werden jeweils nur die Eigenschaften geladen, die benötigt und in Form von Projektionsklassen bereitgestellt werden.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Ne das mit den 2000 war eher auf das Thema Performance bezogen. In der heutigen Lösung werden alle 2000 in einer scrollbaren Liste dargestellt, würde ich so nie wiede realisieren wollen 😃

Über das Contrib Projekt bin ich beim googlen auch gestolpert, hatte aber bzgl. der Aktivität ein ähnliches Gefühl wie du … aber bei den paar Eigenschaften spielt es imho noch keine Rolle.

Das Beispiel war eher auf die einzelnen SQL Eigenschaften bezogen, der Rest ist selbst aus meiner Sicht suboptimal gelöst.

Die UI hatte ich nur ins Spiel gebracht weil ja beim vorherigen Beispiel die Inotify Schnittstelle zusammen mit der ObservableCollection alles "selbst" gemacht hat. Ohne die Schnittstelle habe ich ja das Problem das beispielsweise die Daten die dem Team hinterlegt sind nicht mehr automatisch aktualisiert werden sondern ich ja eigentlich bei einer Änderung die Initialisierung erneut anstossen muss (zumindest bei dem was ich bisher zur List gelesen habe)

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

stehe jetzt komplett aufm Schlauch 😦

Kann mir jemand anhand des neuen Codes zeigen wie mein Code aussehen muss damit bei Änderungen im View (nehmen wir an ich ändere bei einem Mitarbeiter den Nachnamen direkt im Datagrid) die Änderung via OOP in die Datenbank zurück gespeichert werden?

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Habs nach langem Würgen hingekriegt, allerdings glaube ich das es net so "gut" ist.

Hier nur die 3 aktualisierten Dateien:

ViewModel.cs

using SQLiteDapper.Model;
using System;
using System.Collections.Generic;
using System.Windows.Input;

namespace SQLiteDapper.ViewModels
{
    public partial class ViewModel
    {
        public RelayCommand Update { get; set; }
        public List<Employee> Employees { get; set; }
        public Employee MySelectedEmployee { get; set; }
        public ViewModel ()
        {
            // Get data for View Datagrid
            IEmployeeSQLiteRepository employeeRepository = new EmployeeSQLiteRepository();
            Employees = employeeRepository.GetAll(); //per Binding ins XAML 
            this.Update = new RelayCommand(_ => UpdateRecord());
        }
        void UpdateRecord()
        {
            IEmployeeSQLiteRepository employeeRepository = new EmployeeSQLiteRepository();
            Employee emp = employeeRepository.GetById(MySelectedEmployee.ID);
            emp.FirstName = MySelectedEmployee.FirstName;
            emp.LastName = MySelectedEmployee.LastName;
            emp.TeamID = MySelectedEmployee.TeamID;
            employeeRepository.Update(emp);
        }
    }

    public class RelayCommand : ICommand
    {
        private Action<object> action;
        public RelayCommand(Action<object> action)
        {
            this.action = action;
        }
        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            action(parameter);
        }

        public event EventHandler CanExecuteChanged;
    }
}

IEmployeeSQLiteRepository.cs

using System.Collections.Generic;

namespace SQLiteDapper.Model
{
    public interface IEmployeeSQLiteRepository
    {
        List<Employee> GetAll();
        //bool Add(Employee employee);
        Employee GetById(int id);
        Employee Update(Employee employee);
    }
}

EmployeeSQLiteRepository.cs

using System;
using Dapper;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Linq;
using Dapper.Contrib.Extensions;

namespace SQLiteDapper.Model
{
    class EmployeeSQLiteRepository : IEmployeeSQLiteRepository
    {
        private SQLiteConnection db = new SQLiteConnection(Properties.Settings.Default.connString);
        // Get Employee record by Id
        public Employee GetById(int id)
        {
            try
            {
                return this.db.Query<Employee>("SELECT * FROM employees WHERE ID=@Id", new { Id = id }).FirstOrDefault();
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
                return null;
            }
        }
        // Retrieves the data from the table.
        public List<Employee> GetAll()
        {
        try
            { 
                return this.db.Query<Employee>("SELECT * FROM employees LIMIT 20").ToList();
            }
        catch (Exception exception)
            {
                Console.WriteLine(exception);
                return null;
            }
        }
        // Update the employee record
        void Update(Employee employee)
        {
        try
            {
                SqlMapperExtensions.Update(db, new Employee { ID = employee.ID, FirstName = employee.FirstName, LastName = employee.LastName, TeamID = employee.TeamID });

            }
         catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        Employee IEmployeeSQLiteRepository.Update(Employee employee)
        {
            throw new NotImplementedException();
        }
    }
}

So oder so habe ich hierzu ein paar Sachen die mir nicht klar sind:

  • Ist es "sinnvoll" die von Dapper genutzte Liste zu verwenden oder sollte man diese in eine ObservableCollection umwandeln da man dann "INotifyPropertyChanged" nutzen kann? (Würde aber irgendwie Dapper ad absurdum führen in meinen Augen) --> Falls ja, gibt es da eine einfache Möglichkeit?

  • Ich führe jetzt das Update eines Mitarbeiters via Button im WPF Formular aus, ist der Code so korrekt mit der ICommand Schnittstelle?

  • Generell, ist der Code und der Aufbau so in Ordnung oder gibt es irgendwas "schwerwiegendes" was ich noch ändern sollte?

16.807 Beiträge seit 2008
vor 4 Jahren

Datenbank-Objekte werden prinzipiell nicht in UIs verwendet -> Schichtentrennung.
[Artikel] Drei-Schichten-Architektur

Es ist selten, dass Du alle Felder aus einer Datenbank 1:1 auch so in der UI hast.
Zusätzlich stellt das natürlich eine Abhängigkeit dar, dass jede Änderung an der DB sich durch alle Schichten durchschlägt.

Zusätzlich hast Du keinerlei Logikschickt.
Du hast direkt die Datenbank-Schicht in der UI-Schicht.

Ist pragmatisch; aber natürlich auch risikoreich bei Anpassungen etc.

PS: Select * gilt als Bad Practise, weil Du eben alle Spalten liest - die Du aber evtl. gar nicht brauchst.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Danke dir für das Feedback.

Habe heute nochmal alles über den Haufen geworfen und wieder von vorne angefangen da ich nicht zufrieden bin .... Macht 0 Sinn weiter zu machen wenn die grundlegendsten Sachen nicht "stimmen".
Habe jetzt ne Möglichkeit gefunden mit Dapper einigermassen ne ObersavbleCollection raus zu kriegen und damit kann ich wieder die Inotify Schnittstelle nutzen.

  • das mit dem "*" werde ich so ändern wie du sagst, genau die Sachen raus holen die ich brauche
  • das mit der 3 Schichten Architektur schnalle ich nur im Ansatz ein klein wenig aber im Grossen und Ganzen ist das nur ein Fragezeichen für mich (habe schon genug mit MVVM zu tun damit da im Ansatz alles sauber bleibt) aber ich lese mir das mal in aller Ruhe durch und vielleicht kann ich ja einen Teil davon umsetzen

PS: Ich glaube je länger je mehr das so ein Projekt anfangs vielleicht doch 2-3 Nummern zu gross ist...

P
441 Beiträge seit 2014
vor 4 Jahren

Den Namen deines Repository Interface würde ich generischer halten - warum steht das SQLite im Namen, das kann durchaus Technologie Unabhängig sein!

ObservableCollection kannst du in der GUI schicht durchaus verwenden. An einer DB macht es nur dann Sinn, wenn die DB dich über Änderungen informieren würde.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Habe die letzten Tagen viel gelesen und versucht so viel als möglich umzusetzen und das ganze Projektlein etwas "seriöser" umzusetzen. (DAL, etc) Dapper wurde (entgegen des Titels) noch nicht implementiert, das mache ich erst wenn die Methoden und Co so passen.

Hier der Azurelink --> https://moritzjuergensen.visualstudio.com/SQL_V1/SQL_V1%20Team/_git/SQL_V1

Ich habe dennoch einige offenen Punkte die mir nicht einleuchten und bei denen mir vielleicht jemand helfen kann:

  • die Methoden oder Funktionen im Inferface geben ja etwas zurück, wie bestimme ich ob es beispielsweise ein Bool oder ein Wert ist? (Beispiel: Mitarbeiterupdate ... eigentlich will ich ja nur wissen ob es geklappt hat oder net, sprich ein Bool würde mir genügen oder sehe ich das falsch?)
  • wenn jetzt noch mehr Klassen dazu kommen (Beispiel: Projektarten, Projektphasen, etc), macht man dann ein neues Projekt oder "sollte" man Sachen die logisch zusammen passen (Beispiel: Alles was im Konfigurationsmenu ist) in einem Projekt vereinen?

Wäre echt froh wenn Verbesserungen anhand meines Codes dargestellt würden, dann kann ich gleich vergleichen wo ich Fehler gemacht habe.

Vielen Dank an Alle die mir helfen 😃

16.807 Beiträge seit 2008
vor 4 Jahren

wie bestimme ich ob es beispielsweise ein Bool oder ein Wert ist?

Prinzipiell mit dem Rückgabetyp von Methoden.

Im Falle von Datenbank-Operationen gibst Du i.d.R. jedoch das aktualisierte Objekt zurück.
Wenn etwas schief geht, dann wirfst Du eine Exception.

Nur dass etwas schief gegangen ist; das reicht Dir ja nicht.

macht man dann ein neues Projekt oder "sollte" man Sachen die logisch zusammen passen (Beispiel: Alles was im Konfigurationsmenu ist) in einem Projekt vereinen?

[Artikel] Drei-Schichten-Architektur

Was genau in einzelnen Projekten liegt, das kommt auf das Projekt an.
Wenn es für alles immer eine pauschale Lösung geben würde, bräuchte man keine Entwickler und Architekten. 😉

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Würdest du mir anhand meiner Update Employee Methode zeigen wie es aussehen sollte? Dann könnte ich analog die anderen Methoden entsprechend ändern.

Werde den 3 Schichten Artikel nochmals lesen und mir überlegen wie ich das in grösserem Ausmasse umsetzen könnte

16.807 Beiträge seit 2008
vor 4 Jahren

Erstmal rundum:

  • Halte Dich an die C# Guidelines. Parameter in Methoden schreibt man klein; ansonsten schnelle Verwechslungsgefahr mit Eigenschaften.
  • Missbrauche den out-Parameter nicht
  • Warum Dein Projekt keinen richtigen Namen hat sondern SqlDapper heisst, erschließt sich mir nicht. Warum nennst Du es nicht sowas wie Mitarbeiterverwaltung oder sowas?
    Man nennt ja nicht ein Produkt nach dem Namen der eingesetzten Technologie
  • der DAL ist nur eine virtuelle Trennung der Verantwortlichkeiten in einer Anwendung. Namentlich existiert dieser aber nicht; weder als Namespace noch als Klasse
  • NuGet Packages checkt man nicht mit ein, genauso wenig bin/dbg. Da können Informationen drin sein, die Du nicht teilen willst 😃

Du hast hier eine Mini-Anwendung; im Prinzip ist es sehr einfach, wenn Du Dich an die Grundlagen der Guidelines von C# hälst - dazu musst Du sie Dir aber mal durchlesen 😃
Leider machen das die wenigsten, weswegen strukturelle Probleme oft hier schon die Ursache haben.

Wenn man sich an die wichtigsten Puntke (zB .NET Namespaces und Projekte) hält, dann kommt zB sowas bei raus:
https://github.com/BenjaminAbt/Sample.DotNetWPFStructure (einfach als Zip ziehen oder mit Hilfe der OctoTree Browserweriterung bequem anschauen).

Du hast eine Klassenbibliothek mit dem erfundenen Namen "Moritz.Mitarbeiterverwaltung", in der die gesamte Logik Deiner Anwendung untergebracht ist - ohne Abhängigkeiten an die Runtime (hier WPF) zu haben.
Hinzu kommt eben die WPF Anwendung hier als Name DesktopApp - die Runtime gehört schließlich nicht in den Projektnamen.
Die Klassenbibliothek enthält hierbei also die Logik und die Datenbankschicht (DAL) - ohne die Schichten namentlich zu nennen.

Die Namen der Klassen und Interfaces helfen Dir beim Faktor Modularisierung.
Die Logik interessiert nur, dass es ein Repository gibt, mit dem Mitarbeiter veraltet werden können (IEmployeeRepository) und ist hierbei so platziert, dass sie auch im Namespace neutral liegt.

Die konkrete Implementierung liegt nun in einem spezifischen Namespace (Sqlite) und trägt auch einen spezifischen Namen (EmployeeSqliteRepository).
Entscheidest Du Dich später statt Sqlite eben zB MSSQL zu verwenden, dann ist dies sehr einfach durch einen eigenen Namespace erreichbar inkl. den spezifischen Implementierungen (EmployeeMssqlRepository).

Die Namespaces haben dabei die Struktur, dass Du sie jederzeit in extra Projekte auslagern könntest, um eine höhere Unabhängigkeit von Dependencies (eben Mssql, Sqlite...) zu haben.
Lohnt sich dann aber erst wenn man es wirklich braucht.

Dieses Basis ermöglicht dann die Anwendung von Prinzipien wie Dependency Injection und erfüllt die 3- Schicht Architectur.
Du kannst nun nämlich die Logik aus einer WPF-App, Konsolen-App oder Web-App ansprechen.

Zur Update Methode: sofern es sich um eine einfache Anwendung handelt:

  • Entität gegen die Datenbank aktualisieren
  • Entität aus der Datenbank lesen und in der Methode zurück geben

Im Endeffekt müssten aber alle Deine Repositories komplett umgeschrieben werden.
Du hast ein Repository, in dem eigentlich die Datenbank-Operationen liegen sollte, dann aber noch eine DAL Klasse. Erschließt sich mir nicht.

//Add a new employee
        public ObservableCollection<Employee> AddEmployee(ObservableCollection<Employee> OCEmployees)

Die Methode hat namentlich "Füge einen Employee hinzu" - Du gibst ihr aber eine ganze Liste.

Es reicht völlig (mit Dapper, anders macht es keinen Sinn - keiner will Sql Code schreiben)

    class EmployeeSqliteRepository : IEmployeeRepository
    {
	IDbConnection _connection; // Sqlite implementiert IDbConnection. Siehe ADO.NET

	public EmployeeRepository (IDbConnection connection) // Die Connection wird übergeben und nicht im Repository erstellt! Eine Connection können sich mehrere Repository-Instanzen teilen.
	{
		_connection = connection;
	}	

	public IList<EmployeeEntity> GetAll()
	{
		_connection.GetAll<EmployeeEntity>().ToList(); // GetAll = Dapper
	}


        public bool Update(EmployeeEntity e)
        {
		return connection.Update(e);
        }
    }

Und hier sieht man direkt, dass man das ganze generische Schreiben kann (OOP)


    class SqlRepository<TEntity> : ISqlRepository<TEntity>
    {
	IDbConnection _connection;

	public SqlRepository (IDbConnection connection)
	{
		_connection = connection;
	}	

	public IList<TEntity> GetAll()
	{
		_connection.GetAll<TEntity>().ToList(); // GetAll = Dapper
	}


        public bool Update(TEntity e)
        {
		return connection.Update(e);
        }
    }

public class EmployeeRepository : SqlRepository<EmployeeEntity>
{
	public EmployeeRepository (IDbConnection connection) : base(connection)
	{
		
	}
}
public class TeamRepository : SqlRepository<TeamEntity>
{
	public TeamRepository (IDbConnection connection) : base(connection)
	{
		
	}
}

Man also den strukturellen Code nur ein mal schreiben muss.

Im Endeffekt ist das also nur eine Mischung aus den C# bzw. .NET Guidelines und Grundlagen von Objekt-orientierter Programmierung 😃
Aber ja, man muss sich diese Dinge durchlesen, im Kopf sortieren und abstrahieren. Kommt alles mit der Zeit.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Vorab:

Vielen lieben Dank für das mehr als ausführliche Feedback!!
Die Quintessenz daraus ist für mich ganz klar: Nochmals über die Bücher und ganz von vorne anfangen. Irgendwie sind viele Sachen zwar in den Kopf rein aber leider auch wieder raus; hast du mit deinem letzten Satz auch wunderbar getroffen

Aber ja, man muss sich diese Dinge durchlesen, im Kopf sortieren und abstrahieren. Kommt alles mit der Zeit.

Nun zum detaillierten Feedback:

Das mit dem Projektnamen habe ich ehrlich gesagt ignoriert weil ich erst mit Dapper gespielt hatte. Wird natürlich so auch nicht mehr gemacht. Mit meiner Interpretation von DAL war ich wohl auf dem Holzweg, hatte das so in einem Lernvideo gesehen und dachte das es sich so gehört.

Mit dem Einchecken muss ich mich explizit nochmals befassen, hatte einfach den Hauptordner angeklickt und dann veröffentlicht.

(Zum Thema Guidelines durchlesen: Witzig ist das ich mein Accessprogramm erst auch kreuz und quer aufgebaut hatte und erst später eine "organisierte und aufgeräumte" Version erstellt habe .... anstatt direkt von Anfang an ... neige zur Chaostheorie)

Das mit der DAL Klasse ist simpel (wenngleich auch total falsch): Ich dachte ich müsse sämtliche Datenbankzugriffe (den SQL Code) in eine eigene Datei auslagern, deshalb dieser "Murks".

Ich muss mir nochmals ganz in Ruhe die Grundlagen durchlesen und dann deinen Code neu interpretieren.

Der Teil mit den Listen holen und die Entity updaten ist logisch (auch wenn ich den Codesyntax noch nicht 100% verstehe) aber bei "public EmployeeRepository (IDbConnection connection) : base(connection)" ist Schluss. Vermute mit ":base(Connection)" implementierst du etwas wo die Verbindungsdaten geregelt sind, liege ich damit richtig?

Auf alle Fälle, vielen vielen Dank für deine Bemühungen ... weiss es echt zu schätzen 🙂

2.298 Beiträge seit 2010
vor 4 Jahren

aber bei "public EmployeeRepository (IDbConnection connection) : base(connection)" ist Schluss. Vermute mit ":base(Connection)" implementierst du etwas wo die Verbindungsdaten geregelt sind, liege ich damit richtig?

EmployeeRepository erbt von der Basisklasse SqlRepository<EmployeeEntity>. Der Aufruf am Ende des Konstruktors bewirkt, dass der Konstruktor der Basisklasse aufgerufen wird.

Auf diese weise wird ganz gut redundanter Code vermieden und viel wichtiger: Die abgeleitete Klasse muss die Datenzugriffsschicht außer im Konstruktor gar nicht kennen.

Wissen ist nicht alles. Man muss es auch anwenden können.

PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Danke für die Erklärung 😃

@Abt
vielen Dank für den Github Link!

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Ich habe die letzten Tage versucht das Beispiel von Abt (OOP) in das von ihm netterweise zur Verfügung gestellte Rahmenprojekt einzubauen. Mir erschliessen sich noch einige Sachen nicht so wirklich, darum brauche ich (mal wieder) euren Rat.

Die Struktur hat sich leicht verändert im Gegensatz zum Originalprojekt (siehe Bild im Anhang)

Der Inhalt der einzelnen Dateien sieht nun wie folgt aus:
EmployeeEntity.cs

namespace Toolbox.Employee.Database.Entities
{
    class EmployeeEntity
    {
    }
}

EmployeeRepository.cs

using System.Data;
using Toolbox.Employee.Database.Entities;

namespace Toolbox.Employee.Database.Sqlite.Repositories
{
    class EmployeeRepository : SqlRepository<EmployeeEntity>
    {
        public EmployeeRepository(IDbConnection connection) : base(connection)
        {
            GetAll();  //BEISPIEL
        }
    }
}

IEmployeeRepository.cs

namespace Toolbox.Employee.Database.Sqlite.Repositories
{
    class IEmployeeRepository
    {
    }
}

ISqlRepository.cs

namespace Toolbox.Employee.Database.Sqlite.Repositories
{
    internal interface ISqlRepository<TEntity>
    {
    }
}

SqlRepository.cs

using Dapper.Contrib.Extensions;
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace Toolbox.Employee.Database.Sqlite.Repositories
{
    class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class
    {
        IDbConnection _connection;

        public SqlRepository(IDbConnection connection)
        {
            _connection = connection;
        }

        public IList<TEntity> GetAll()
        {
            return _connection.GetAll<TEntity>().ToList(); // GetAll = Dapper
        }

        public bool Update(TEntity e)
        {
            return _connection.Update(e);
        }
    }

}

DbSqliteContext.cs

namespace Toolbox.Employee.Database.Sqlite
{
    class DbSqliteContext
    {
    }
}

IDbContext.cs

namespace Toolbox.Employee.Database
{
    class IDbContext
    {
    }
}

Employee.cs

using System.ComponentModel;

namespace Toolbox.Employee.Models
{
    public class Employee
    {
        //Employee ID -- Auto Increment (Database)
        public int Id { get; set; }
        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set
            {
                if (_firstName != null && _firstName != value)
                {
                    _firstName = value;
                }
            OnPropertyChanged("FirstName");
            }
        }
        private string _lastName;
        public string LastName
        {
            get => _lastName;
            set
            {
                if (_lastName != null && _lastName != value)
                { 
                    _lastName = value;
                }
                OnPropertyChanged("LastName");
            }
        }
        //TeamId -- Foreign Key
        private int _teamId;
        public int TeamId
        {
            get => _teamId;
            set
            {
                if (_teamId != value)
                    { 
                    _teamId = value;
                    }
                else
                {
                    return;
                }
                OnPropertyChanged("TeamId");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Team.cs

namespace Toolbox.Employee.Models
{
}

EmployeeService.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Toolbox.Employee.Services
{
    class EmployeeService
    {
    }
}

Soweit die Theorie, nun habe ich mir dazu ein paar Gedanken gemacht, recherchiert und mir sind einige Dinge unklar und bräuchte weitere Hilfe:

  • um eine generische Methode nutzen zu können muss ich ja eine Entity definieren. Ist es richtig das ich in der Datei "EmployeeEntity.cs" definiere aus was diese Entity besteht (beispielsweise ID, Vorname, Nachname) und dann in der Datei "Employee.cs" beschreibe das der Vorname ein String ist und wie get/set definiert sind?

  • damit ich die IDbConnection "connection" nutzen kann muss ich ja irgendwie definieren wohin sie connecten soll. Wo wird das definiert (in welcher Datei) und vor Allem, die Verbindung sollte ja nach erfolgtem Update oder was auch immer wieder geschlossen werden.

  • wenn ich in der Datei "EmployeeRepository.cs" "GetAll" (gekennzeichnet mit Beispiel) aufrufen will, warum ist dort keine Angabe der Entity notwendig? (oder ist es so wie ich vermute und ich starte von dort gar nicht den Aufruf?)

  • ausgehend von der MVVM Idee, entspricht "EmployeeService.cs" dem ViewModel für die Employee Sachen?

16.807 Beiträge seit 2008
vor 4 Jahren

Ist es richtig das ich in der Datei "EmployeeEntity.cs" definiere aus was diese Entity besteht (beispielsweise ID, Vorname, Nachname) und dann in der Datei "Employee.cs" beschreibe das der Vorname ein String ist und wie get/set definiert sind?

Si.

public class EmployeeEntity : Entity
{
    public int Id {get;set;}
    public string Vorname {get;set;}
    public string Nachname {get;set;}
    // ...
} 

Wo wird das definiert (in welcher Datei) und vor Allem, die Verbindung sollte ja nach erfolgtem Update oder was auch immer wieder geschlossen werden.

Eigentlich übernimmt das eben die Dependency Injection für Dich.
services.AddTransient<IDbConnection>(_ => return new SqliteConnection("connection string..)".

Verwendest Du keine Dependency Injection aktuell, dann musst Du das selbst instantiieren und auch selbst verwalten (Disposen=>Closen).

Oft wrappt man die Verbindung selbst in einem "DbContext".
D.h. der DbContext kennt die Verbindung und das Repository der DbContext.
Der DbContext hat darüber hinaus die Kenntnis über alle Tabellen, die die Repositories nutzen können.
Das ist das, was Du da im Screenshot auch siehst.

warum ist dort keine Angabe der Entity notwendig

Generics => Generic Classes (C# Programming Guide)

  • ausgehend von der MVVM Idee, entspricht "EmployeeService.cs" dem ViewModel für die Employee Sachen?

Nein. Ein Service ist Bestandteil der Logik, nicht der View.

Das ViewModel kann/darf aber den Service kennen und kann drauf zugreifen.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Woher kommt denn das ": Entity"?

public class EmployeeEntity : Entity

Müsste ich das nicht auf das Employee.cs File im Model Ordner referenzieren?

16.807 Beiträge seit 2008
vor 4 Jahren

Ah, ist hier gar nicht drin - sorry.

Man kann eben Vererbung nutzen, um generische Einschränkunge zu realisieren.

Wenn man folgendes schreibt...

class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class

.. dann kann man hier als TEntity alles einwerfen, was eine Klasse ist.

Wenn man schreibt

class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, Entity

schreibt, dann man kann nur Klassen als Generic übergeben, die von Entity erben (bzw. kann man auch einfach als Interface deklarieren).


public interface IEntity {}
public abstract class Entity : IEntity {}

public class EmployeeEntity: Entity {}

class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity // oder Entity wenn man kein Interface hat
M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

irgendwas geht sich bei mir net aus.

Versuche mal aufzuzeigen wo meine Denkblockade ist:

In der Datei "SqlRepository.cs" sind die Datenbankaugaben wie "alle Einträge holen" oder "Eintrag updaten" abgelegt. Der Code

    class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, Entity
    {
        //BLIBLABLUB
    }

besagt das es sich um eine Klasse "SqlRepository" handelt die als Parameter eine TEntity erfordert (stellt in diesem Fall Mitarbeiter oder Team dar), die vom Interface "ISqlRepository" abhängig ist und der Parameter nur gültig ist wenn es eine Klasse ist die von Entity erbt -- in deinem Beispiel

public class EmployeeEntity: Entity {}

Die Klasse "EmployeeEntity" erbt von Entity und sie wie folgt aus:

public class EmployeeEntity : Entity
{
    public int Id {get;set;}
    public string Vorname {get;set;}
    public string Nachname {get;set;}
    // ...
}

Sprich hier ist definiert welche Eigenschaften vorhanden sind

Widerspruch 1 für mich: Wieso brauche ich denn ein Model "Employee.cs" in dem fast das Gleiche steht? Kann es ja net doppelt definieren --> Annahme: Employee.cs fällt weg

Widerspruch 2 für mich: Das mit dem " : Entity" Zusatz verstehe ich gar nicht. Was soll denn da drin stehen?--> Annahme: Gemäss folgendem Link (abstrakte Klasse) vermute ich das diese Klasse Sachen beinhaltet die für beide Entities (Team oder Mitarbeiter) relevant sind, wobei sich mir hier net erschliesst was das sein soll

Die ganz ganz groben Züge erschliessen sich mir langsam aber irgendwie bleibt jedesmal ein Fragezeichen, daher vielen Dank das Ihr meine "blöden" Fragen so umfassend beantwortet 😃
Hoffe das ich mir eurer Hilfe bald eine funktionierende Klassenbibliothek habe mit der ich das ganze nochmals durchgehen kann; sprich den kompletten Workflow innerhalb der Klasse abarbeiten kann.

16.807 Beiträge seit 2008
vor 4 Jahren

Widerspruch 1 für mich: Wieso brauche ich denn ein Model "Employee.cs" in dem fast das Gleiche steht?

Das mag in Deinem sehr einfachen Beispiel so sein; die Regel ist, dass Entität und Modell nicht identisch sind.
Beispiel: Du hast in einer UserEntity zB. E-Mail und Password.
Das willst Du mit Sicherheit nicht in Deinem Model "User" haben.

Aber wie bereits gesagt: pragmatisch bleiben ist legitim.

Das mit dem " : Entity" Zusatz verstehe ich gar nicht. Was soll denn da drin stehen?

Das sind generische Einschränkungen.
Siehe Erklärung oben. Das ist ein wichtiges Element in OOP von/mit .NET.

Damit schränkst Du eben ein, dass eine Klasse nur mit den generischen Klassen funktioniert, die Du definierst.
Du musst Dich wirklich ein wenig mit den Themen beschäftigen...

Aber hier nochmal:

class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity

class EmployeeRepository : SqlRepository<EmployeeEntity>, IEmployeeRepository


Damit definierst Du, dass TEntity eine Klasse sein muss, die das Interface IEntity implementiert.

public class EmployeeEntity : Entity // Entiy implementiert IEntity

wird nun funktionieren, weil Du die Constrainst erfüllst.

public class EmployeeEntity {}

wird ein Fehler verwerfen, weil Du nirgends IEntity implementierst.
In diesem Fall führen die Constraints eben dazu, dass Du garantiert in einem Repository eine Klasse verwenden musst, die zwangsweise eine Entität in Form der Implementierung von IEntity ist.

Das ganze kann man natürlich jetzt noch weiter Spinnen, dass Du IEntity gewisse Inhalte gibst.

public interface IEntity
{
   int Id {get;}
}

Du gibst also zB Deiner Entität immer vor, dass sie eine Eigenschaft id hat, die immer ein int ist.
Das sorgt dafür, dass Du in Deinem SqlRepository bereits auf die Eigenschaft id zugreifen kannst ohne die konkrete Klasse zB Employee zu kennen.
Damit kannst Du allen ableitenden Repositories zB ein GetById() zur Verfügung stellen ohne das jedes Mal im spezifischen Repository schreiben zu müssen - was aber eben die Gemeinkeit jeder Entität voraussetzt, dass es immer eine Id-Spalte gibt.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Aber wie bereits gesagt: pragmatisch bleiben ist legitim.

Ja ich muss irgendwo ne Grenze ziehen, ansonsten habe ich zu viel drin was ich nicht wirklich verstehe. Habe jetzt schon Schwierigkeiten den ganzen Sachen zu folgen.

Du musst Dich wirklich ein wenig mit den Themen beschäftigen...

und damit wären wir bei Problem Nummer 2: Ich für meinen Teil finde es extrem schwierig anhand von abstrakten Erklärungen daraus die richtigen Schlussfolgerungen zu ziehen, zumal mir hier im RL auch niemand wirklich helfen kann. Beispielsweise dein Link zu den generischen Klassen.
Klar steht dort sehr viel und vermutlich auch sehr gut erklärt aber für mich als Laien doch schwer zu interpretieren und dann in ein funktionierendes Beispiel einzusetzen.

Wenn ich ein konkretes Beispiel habe (wie jetzt in meinem Fall die aus deiner Sicht sicherlich sehr simple Anwendung) dann kann ich anhand meines Codes die Theorie nachvollziehen, resp. ich kann es vermutlich besser als andersrum.

Die Idee mit "GetByID" greife ich auf, zumal ich in meiner Anwendung in jeder Tabelle eine eindeutige ID habe und auch haben werde.

Vielen Dank für deine Geduld! 😃 Ich denke ich werde jetzt erstmal wieder über die Bücher und versuche deinen umfangreichen Input irgendwie zu interpretieren und dann umzusetzen bevor ich auch nur daran denken sollte weiter zu machen. Werde auch im Buch weiter machen und parallel die Grundlagen nochmals durchlesen und vertiefen.

16.807 Beiträge seit 2008
vor 4 Jahren

Die Idee mit "GetByID" greife ich auf, zumal ich in meiner Anwendung in jeder Tabelle eine eindeutige ID habe und auch haben werde.

Nicht nur eine eindeutige Id, sondern die Spalte muss auch überall identisch heißen (eben Id).

Am Ende vom Tag kann das so aussehen:

    public interface IDbContext
    {

    }

    public class DbContext : IDbContext
    {

    }

    public interface IEntity
    {
        int Id { get; }
    }

    public abstract class Entity : IEntity
    {
        public int Id { get; set; }
    }

    public interface ISqlRepository<TEntity> where TEntity : class, IEntity
    {
        IList<TEntity> GetAll();
        int Count();
        TEntity GetById(int id); // Wir wissen ja, Id muss int sein
        bool Delete(TEntity entity);
        bool Delete(int id);
    }

    public abstract class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity
    {
        protected IDbContext DbContext { get; }
        protected abstract string TableName { get; }
        protected abstract string IdFIeld { get; }

        protected SqlRepository(IDbContext dbContext)
        {
            DbContext = dbContext;
        }

        public IList<TEntity> GetAll()
        {
            return DbContext.Connection.GetAll<TEntity>();
        }

        public TEntity GetById(int id)
        {
            return DbContext.Connection.Get<TEntity>(id);

            // wenn man kein Dapper.Contrib hat/will:
             string sql = $"SELECT * FROM {TableName} WHERE {IdFIeld} = @Id";
             TEntity entity = DbContext.Connection.Query<TEntity>(sql, new { Id = entity.Id }); // hier braucht man eben nun die definierte Id von IEntity.Id
             return entity;
        }

        public int Count()
        {
            string sql = $"SELECT COUNT(*) FROM [{TableName}];";
            return DbContext.Connection.ExecuteScalar<int>(sql)
        }

        public bool Delete(TEntity entity)
        {
            return DbContext.Connection.Delete(entity)

            // wenn man kein Dapper.Contrib hat/will:
             var sql = $"DELETE FROM {TableName} WHERE {IdFIeld} = @Id";
             var affectedrows = DbContext.Connection.Execute(sql, new { Id = entity.Id }); // hier braucht man eben nun die definierte Id von IEntity.Id
        }

        public bool Delete(int id)
        {
            var sql = $"DELETE FROM {TableName} WHERE {IdFIeld} = @Id";
            var affectedrows = DbContext.Connection.Execute(sql, new { Id = id });

            return affectedrows > 0;
        }
    }


    public class EmployeeEntity : Entity
    {
        public string Vorname { get; set; }
        public string Nachname { get; set; }
    }

    public interface IEmployeeRepository : ISqlRepository<EmployeeEntity>
    {

    }

    public class EmployeeRepository : SqlRepository<EmployeeEntity>, IEmployeeRepository
    {
        public EmployeeRepository(IDbContext dbContext) : base(dbContext)
        {
        }

        // Tabellenname für selbst geschriebene SQL Commands, die Dapper.Contrib nicht unterstützt
        // ansonsten muss eben in Dapper.Contrib via Attribute oder Mapper Configuration der Tabellenname hinterlegt werden
        protected override string TableName { get; } = "Employees";
        protected override string IdField { get; } = "Id";
    }

Ein mal die richtige OOP Basis und Du hast nur noch den Aufwand für das Definieren der Enitäten und das Anlegen des Repositories.
Die Standard SQL Methoden bekommst Du dann durch die Vererbung einfach geschenkt.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Entschuldige die verspätete Antwort, krankheitsbedingt ist dies leider völlig in Vergessenheit geraten 😠

Ich habe die letzten Wochen versucht mittels des gekauften Buches die Grundlagen zu erlangen und mittlerweile leuchtet mir einiges ein was vorher unklar war. Habe mir jetzt dein OOP Beispiel vorgenommen und versuche im Moment jeden Schritt zu dokumentieren (warum und wieso) sowie nachzuvollziehen, was da genau abläuft, sprich wer gibt wem die Anweisung oder wer erbt von wem und was erbt er (und so weiter)

Mein jetziger Stand ist folgender:
Die Grundsatz Idee hinter deinem Ansatz ist die Definition einer generischen Struktur, sprich ich definiere die Funktionen (Löschen, Auslesen, etc) und je nachdem ob es ein Team oder ein Mitarbeiter ist wird immer dieselbe Funktion aufgerufen, nur die "Parameter" (wie zum Beispiel TableName) werden entsprechend geändert.

Um zu Prüfen ob alles funktioniert wollte ich im CodeBehind der XAML Datei eine Messagebox einbinden welche den Vor- und Nachnamen des Mitarbeiters mit der Id 1 ausgibt. Wäre jetzt über das EmployeeRepository eingestiegen aber ich kriege den Befehl net hin. (Vermute der Schluss heisst dann irgendwas a la GetById(1).FirstName😉

PS: der Kram im CodeBehind dient nur zum Testen, habe miich noch nicht mit XAML beschäftigt

PSS:
Hab das ganze Projekt neu hochgeladen, ist einfacher da was nachzugucken
--> https://moritzjuergensen.visualstudio.com/_git/Calendar?path=%2FCalendar&version=GBmaster
Den Connection String habe ich unter "Projekteigenschaften - Ressourcen" definiert. Die DbContext Datei habe ich erstellt, aber das "Set" fehlt mir noch, vielleicht kannste mir hier auch noch auf die Sprünge helfen damit ich anhand eines funktionierenden Modells das ganze mittels Buch nochmals rekapitulieren kann...Falls du mal in der Gegend um Lindau oder der Ostschweiz bist lade ich dich für die Hilfe mal auf ein Bier ein 👍

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

hab versucht anhand von einem Tutorial die "Dependency Injection" einzubauen, allerdings stimmt da etwas noch nicht.

Wenn ich es start wirft die Datei "SqlRepository" einen "System.NullReferenceException: "Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt." Fehler aus. Hab da wohl etwas falsch eingebaut 😭

16.807 Beiträge seit 2008
vor 4 Jahren

Null Reference kann aber nich vom SqlRepository kommen.
Von SqlRepository kann auch gar keine Instanz erzeugt werden, weil es eine abstrakte Klasse ist.

Wo wird die NullReference genau geworfen?

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Die Return Zeile des folgenden Codes wirft die Exception aus (in der Dateil SqlRepository.cs):

        public TEntity GetById(int id)
        {
            return DbContext.Connection.Get<TEntity>(id);
        }

Ich habe die DbContext um folgenden Code ergänzt:

using System.Data.SQLite;

namespace Calendar.Database.DbContext
{
    public class DbContext : IDbContext
    {
        public SQLiteConnection Connection
        {
            get
            {
                return null;
            }
            set
            {
                using (var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;"))
                {
                    connection.Open();
                }
            }
        }
    }
}

Eine Datei namens ContainerConfig.cs beinhaltet das NuGet Paket Autofac für die Dependency Injection und der Code sieht so aus:

using Autofac;
using Calendar.Database.DbContext;
using Calendar.Database.Repositories;

namespace Calendar
{
    public static class ContainerConfig
    {
        public static IContainer Configure()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<DbContext>().As<IDbContext>();
            builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>();
            return builder.Build();
        }
    }
}

Ich war nach dem Tutorial der Meinung das der Code (vor Allem der Teil in der Klammer)

        public EmployeeRepository(IDbContext dbContext) : base(dbContext)
        {
        }

automatisch die Injection triggert aber ich bin da wohl auf dem Holzweg und geh da heute Abend nochmal über die Bücher.

Der genaue Fehlercode lautet übrigends:

System.NullReferenceException
  HResult=0x80004003
  Nachricht = Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
  Quelle = Dapper
  Stapelüberwachung:
   bei Dapper.SqlMapper.<QueryImpl>d__140`1.MoveNext() in C:\projects\dapper\Dapper\SqlMapper.cs: Zeile1066
   bei System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   bei System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   bei Dapper.SqlMapper.Query[T](IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in C:\projects\dapper\Dapper\SqlMapper.cs: Zeile721
   bei DapperExtensions.DapperImplementor.GetList[T](IDbConnection connection, IClassMapper classMap, IPredicate predicate, IList`1 sort, IDbTransaction transaction, Nullable`1 commandTimeout, Boolean buffered)
   bei DapperExtensions.DapperImplementor.Get[T](IDbConnection connection, Object id, IDbTransaction transaction, Nullable`1 commandTimeout)
   bei System.Dynamic.UpdateDelegates.UpdateAndExecute5[T0,T1,T2,T3,T4,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
   bei DapperExtensions.DapperExtensions.Get[T](IDbConnection connection, Object id, IDbTransaction transaction, Nullable`1 commandTimeout)
   bei Calendar.Database.Repositories.SqlRepository`1.GetById(Int32 id) in C:\Users\mj\Source\Repos\Calendar_Azure\Calendar\Calendar\Database\Repositories\SqlRepository.cs: Zeile27
   bei Calendar.MainWindow..ctor() in C:\Users\mj\Source\Repos\Calendar_Azure\Calendar\Calendar\ViewModels\MainWindow.xaml.cs: Zeile24

16.807 Beiträge seit 2008
vor 4 Jahren
     public SQLiteConnection Connection
        {
            get
            {
                return null;
            }
            set
            {
                using (var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;"))
                {
                    connection.Open();
                }
            }
        }

Das kann nicht klappen; das macht auch null sinn und im gleichen Zug kann der Zugriff hier nur null ergeben und eine Exception werfen.
Siehe auch [FAQ] NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt - kann man auch super debuggen ( [Artikel] Debugger: Wie verwende ich den von Visual Studio? )

public SQLiteConnection Connection
        {
            get
            {
                var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;");
                connection.Open();
                return connection;
            }
        }

Hier darf auch kein using() verwendet werden, sonst ist die Verbindung natürlich sofort wieder verworfen.
usings() verwendet man so, dass sie um die eigentliche Operation liegen.

Noch besser natürlich

public class SqliteContext : IDbContext
{

   private IDbConnection _connection = null;
   private string _connectionString;
   public SqliteContext(string connectionString)
   {    
         _connectionString = connectionString;
   }
   public IDbConnection Connection
   {
      get
      {
         if (_connection is null)
         {
            _connection = new SQLiteConnection(_connectionString);
            _connection.Open();
         }
         return _connection;
      }
   }
}

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Das mit dem Debugging hatte ich mittels Einzelschritt versucht (und eben auch um heraus zu finden, wann er wohin springt) aber wenn natürlich der Code an sich zwar ok aber trotzdem Blödsinn ist dann bringt dir das nicht viel.

Deine 2. Variante erinnert mich stark an mein Dependency Injection Beispiel von gestern, guck es mir daheim mal an.

A propos, den Teil mit "GetAll()" in SqlRepository.cs musste ich deaktivieren weil GetAll ein IEnumerable zurück gibt und daher

        public IList<TEntity> GetAll()
        {
            return DbContext.Connection.GetAll<TEntity>();
        }

nicht funktionieren kann (wenn ich mich nicht komplett täusche).

Nochmals vielen Dank für die tatkräftige Unterstützung!

Eine kleine Randnotiz noch:
Der Code funktioniert soweit, allerdings habe ich die lustige Eigenschaft dass die Tabelle im db File auf Entity enden muss, sprich "EmployeeEntity". Das finde ich aber raus warum das so ist. 😉

16.807 Beiträge seit 2008
vor 4 Jahren

A propos, den Teil mit "GetAll()" in SqlRepository.cs musste ich deaktivieren weil GetAll ein IEnumerable zurück gibt und daher..

Das eine hat mit dem anderen nichts zutun.
Schau Dir an wie Listen funktionieren, dann siehst Du, dass IList von IEnumerable erbt.
Dann verstehst Du auch, wie hier der korrekte Weg wäre.
Intermediate Materialization (C#)

Darüber hinaus ist es aber nur sehr selten eine gute Idee wirklich alles aus der Datenbank zu laden.
Daher sollte man GetAll eh vermeiden, wenn möglich.

Das finde ich aber raus warum das so ist. 😉

Steht in der Doku.

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

Sodele, mittlerweile habe ich die "Add" und die "Update" Variante auch eingebaut und es klappt wunderbar!

Add (hier musste ich allerdings "long" anstatt "int" nehmen - int wurde in der Dapper Doku genannt - warum kann ich nicht sagen, aber long funktioniert)

        public long Add(TEntity entity)
        {
            return DbContext.Connection.Insert(entity);
        }

Update

        public bool Update(TEntity entity)
        {
            return DbContext.Connection.Update<TEntity>(entity);
        }

Das 2. Beispiel für den Inhalt der DbContext habe ich mal angeguckt:
"noch besser weil die Connection zuerst geprüft wird (sprich ist bereits eine offene Verbindung vorhanden?) und weil es nun als SqLite Verbindungsdatei erkennbar ist?

Mir ist noch aufgefallen das nun die Datei "Employee.cs" (mein ursprüngliches Model) gar nicht mehr eingebunden ist. Gehe ich richtig in der Annahme das nun die Definition (als Beispiel)

        public string FirstName
        {
            get => _firstName;
            set
            {
                if (_firstName != null && _firstName != value)
                {
                    _firstName = value;
                }
                OnPropertyChanged("FirstName");
            }
        }

direkt in der EmployeeEntity gemacht wird und ich die "normale" Model Datei nicht mehr brauche?

16.807 Beiträge seit 2008
vor 4 Jahren

"noch besser weil die Connection zuerst geprüft wird (sprich ist bereits eine offene Verbindung vorhanden?) und weil es nun als SqLite Verbindungsdatei erkennbar ist?

  • Weil der ConnectionString nicht hard im Code ist
    Weil Du den Context so an vielen Stellen re-usen kannst aber keine neue Verbindung geöffnet wird (kann wichtig sein, wenn Du mehrere Operationen hast, die gemeinsam abgeschickt werden sollen - daher zB auch Standardverhalten vom Entity Framework).

Gehe ich richtig in der Annahme..

Das eine hat mir dem anderen nicht viel zutun.
Das sind zwei getrennte Schichten.
[Artikel] Drei-Schichten-Architektur

M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren

ach so, ne den Connection String habe ich auch im 2. Beispiel bereits nicht mehr hard im Code. Hab den in der App.config verstaut (kopiere im Moment bei jedem Debug eine Datei in den bin Ordner zum ausprobieren)

 <connectionStrings>
    <add name="SQLite" connectionString="Data Source=Database\\DatabaseFile\\Database.db;Version=3;" />
  </connectionStrings>

Also gehört eine Entity gemäss Link zu Datenzugriffsschicht da damit ja eigentlich nur verwaltet werden, richtig? (Es wird ja nix verarbeitet sondern nur "verwaltet", resp. aus der DB ausgelesen - das SqlRepository gehört demnach auch dazu, oder?)

Was sich mir nicht ganz ausgeht:
Wenn ich nun in der Entity Definition

public string FirstName { get; set; }

schreibe und gleichzeitig in der Employee Model Datei

        public string FirstName
        {
            get => _firstName;
            set
            {
                //blub blub
            }
        }

steht so ist das doch doppelt gemoppelt, oder nicht? Ich mein ich definiere 2x das es einen String namens "FirstName" gibt.

Viel wichtiger ist, wo wird denn die Model Datei eingeklinkt? In der Entity steht ja

public class EmployeeEntity : Entity

sprich "FirstName" und "LastName" hole ich aus der EmployeeEntity und die "Id" aus der "normalen" Entity. Eigentlich müsste sich doch das Employee Model auf die Entität stützen, oder?

--> Habe nur folgendes Beispiel gefunden, hab aber keine Ahnung ob das so korrekt ist:
https://stackoverflow.com/questions/30319584/how-to-map-entity-framework-model-classes-with-business-layer-class-in-n-tier-ar

5.657 Beiträge seit 2006
vor 4 Jahren

Damit dieses Thema nicht endlos ausufert, mache ich jetzt hier zu. Das hat alles nichts mehr mit einem Code-Rieview zu tun. Bitte beachte die Regeln für dieses Forum: Code-Review Regeln

Bei konkreten Fragen bitte im passenden Unterforum posten, und auch da bitte [Hinweis] Wie poste ich richtig? beachten, besonders 1.2 Nur ein Thema pro Thread.

Weeks of programming can save you hours of planning

Thema geschlossen