Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Erstes C#, MVVM und SQLite Projekt
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

Erstes C#, MVVM und SQLite Projekt

beantworten | zitieren | melden

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 :)
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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!
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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)
private Nachricht | Beiträge des Benutzers
KroaX
myCSharp.de - Member

Avatar #avatar-4080.jpg


Dabei seit:
Beiträge: 315
Herkunft: Köln

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5960
Herkunft: Leipzig

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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 :) )
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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 [email protected]", 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 :( )?
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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?
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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)
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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?
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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 [email protected]", 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?
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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...
private Nachricht | Beiträge des Benutzers
Papst
myCSharp.de - Experte



Dabei seit:
Beiträge: 351
Herkunft: Kassel

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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 :)
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

Zitat von Moritz83
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.
Zitat von Moritz83
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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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
Zitat
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
private Nachricht | Beiträge des Benutzers
inflames2k
myCSharp.de - Experte

Avatar #AARsmmPEUMee0tQa2JoB.png


Dabei seit:
Beiträge: 2332

beantworten | zitieren | melden

Zitat von Moritz83
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 | Spielkartenbibliothek
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

Danke für die Erklärung :)

@Abt
vielen Dank für den Github Link!
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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?
Attachments
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

Zitat von Moritz83
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;}
    // ...
} 
Zitat von Moritz83
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.
Zitat von Moritz83
warum ist dort keine Angabe der Entity notwendig
Generics => Generic Classes (C# Programming Guide)
Zitat von Moritz83
- 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.
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

Woher kommt denn das ": Entity"?

public class EmployeeEntity : Entity

Müsste ich das nicht auf das Employee.cs File im Model Ordner referenzieren?
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Moritz83
myCSharp.de - Member



Dabei seit:
Beiträge: 50

Themenstarter:

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15618
Herkunft: BW

beantworten | zitieren | melden

Zitat von Moritz83
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.
Zitat von Moritz83
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.
private Nachricht | Beiträge des Benutzers