Laden...

2 Entities für das ViewModel aufbereiten

Erstellt von Moritz83 vor 4 Jahren Letzter Beitrag vor 4 Jahren 2.295 Views
M
Moritz83 Themenstarter:in
50 Beiträge seit 2013
vor 4 Jahren
2 Entities für das ViewModel aufbereiten

Moin,

ich habe in meinem Projekt 2 Entities, einmal als Employee (Id, FirstName, LastName und DepartmentFK) und einmals als Departments (Id, Department). In der SQLite Datei sind DepartmentFK und Id (von Departments) verknüpft.

Um die Daten im UI nutzen zu können kann ich ja je eine ObservableCollection erstellen. Soweit ist mir das auch klar, nun will ich aber in (beispielsweise) einer ListView etwas darstellen was wie folgt aussieht:

FirstName - LastName - Department (sprich entsprechend DepartmentFK in der Employees Tabelle hole ich den Namen aus der Departmentstabelle)

Ich habe das nun wie folgt gelöst (funktioniert - bin mir aber nicht sicher ob das der "richtige" Weg ist:

1.) Definition einer neuen Entity CS Datei (abgeleitet von EmployeeEntity, aber halt mit dem Department)

        public class DisplayEmployeeEntity : EmployeeEntity
    {
        public DepartmentEntity Department { get; set; }
    }

2.) Im ViewModel eine neue ObservableCollection von "DisplayEmployeeEntity" erstellen

 public ObservableCollection<DisplayEmployeeEntity> DisplayEmployees { get; }
        public EmployeeViewModel()
        {
            DisplayEmployees = new ObservableCollection<DisplayEmployeeEntity>();
            foreach (var emp in Employees)
            {
                DisplayEmployees.Add(
                new DisplayEmployeeEntity()
                {
                    Id = emp.Id,
                    FirstName = emp.FirstName,
                    LastName = emp.LastName,
                    DepartmentFK = emp.DepartmentFK,
                    Department = Departments.Where(x => x.Id == emp.DepartmentFK).FirstOrDefault()
                });
            }
        }

3.) DisplayEmployees an die Listview in der View binden

Kann man das so lösen oder sollte ich einen anderen Weg gehen?
Mein anderer Ansatz war ein Property direkt in der Employee Entity zu erstellen (Derpartment) und das dann auszuschliessen (arbeite mit Dapper)

  [Computed]
  public DepartmentEntity Department { get; set; }

(so könnte ich ein SelectedItem direkt wieder updaten da es sich um die gleiche Entity handelt)

16.807 Beiträge seit 2008
vor 4 Jahren

Eine Id erzeugt entweder der DAL (Guid) oder die Datenbank (Int).
Prinzipiell wäre dafür der richtige Platz beim Repository Pattern eben eine solche Add Methode.

Dass Du die Logikschicht hier völlig weglässt; das kann man machen - bringt aber oft - gerade beim Hinzufügen von Elementen, Probleme in Form von Abhängigkeiten, Logik oder redundantem Code.
[Artikel] Drei-Schichten-Architektur

2.207 Beiträge seit 2011
vor 4 Jahren

Hallo Moritz83,

zu dem von Abt Gesagtem: Ich würde mir genau für solche Fälle immer ein ViewModel bereitlegen und das entsprechend Mappen. Ein "xyzEntity" sollte nicht in einer ObservableCollection vorkommen. Wenn du eine View hast, dann bau dir ein ViewModel dazu, mappe es entsprechend und zeige es dann an.

Gruss

Coffeebean

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

@Abt
Das mit der ID war mir klar, hab nur deshalb übergeben damit ich beim zurückspielen eine ID zum updaten habe (generische Dapper Methode, übergebe eine EmployeeEntity). Hat halt auf diese Weise geklappt auch wenn es ziemlich wild aussieht.

@Coffeebean
Werde dazunochmals über die Bücher, habe das Zusammenspiel der einzelnen Elemente noch nicht begriffen. Gerade das Thema "ViewModel + Entity + Model" erschliesst sich mirnoch nicht ganz (Wer spricht mit wem, wer erbt von wem, mappen, etc)

Danke für euren Input 👍

16.807 Beiträge seit 2008
vor 4 Jahren

@Gerade das Thema "ViewModel + Entity + Model" erschliesst sich mirnoch nicht ganz (Wer spricht mit wem, wer erbt von wem, mappen, etc)

Steht prinzipiell alles in [Artikel] Drei-Schichten-Architektur 😃
Haben wir Dir ja schon mehrfach verlinkt; aber falls Du es nicht gesehen hast: Da ist auch nen Bild wer mit wem Spricht.

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

ne das Bild kenne ich natürlich, auch den Artikel habe ich gelesen. Bei mir liegt das Problem eher darin die Theorie in die Praxis umzusetzen. Aber das wird schon, Rom ist auch nicht an einem Tag erbaut worden 🙂

Bin dankbar für solche Inputs eurerseits 👍

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

Ich würde mir genau für solche Fälle immer ein ViewModel bereitlegen und das entsprechend Mappen. Ein "xyzEntity" sollte nicht in einer ObservableCollection vorkommen.

Habe jetzt die letzten Stunden versucht raus zu kriegen wie das funktionieren soll, bin aber gescheitert. Die Idee hinter dem Mapping ist es ja die Properties von einem Objekt auf ein anderes zu kriegen OHNE jedesmal den ganzen Code bei Änderungen absuchen zu müssen.

Ich habe mal versucht was zu schustern, aber beim Versuch etwas vom ViewModel via Entity in die DB zu werfen bin ich kläglich gescheitert gescheitert. Mein Versuch sieht so aus:
die definerten Properties:

        #region Properties
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int DepartmentFK { get; set; }
        #endregion

und das Mapping

            //Mapping
            var config = new MapperConfiguration(cfg => cfg.CreateMap<EmployeeViewModelNew, EmployeeEntity>());
            var mapper = config.CreateMapper();
            //Source
            EmployeeViewModelNew EmployeeMapped = new EmployeeViewModelNew
            {
                FirstName = "Test",
                LastName = "User",
                DepartmentFK = 1
            };
            //Target
            EmployeeEntity newEmployee = new EmployeeEntity();
            //Sending to SQLite
            var container = ContainerConfig.Configure();
            using (var scope = container.BeginLifetimeScope())
            {
                var Sending = scope.Resolve<IEmployeeRepository>();
                Sending.Add(EmployeeMapped);
            }

Sodele, das Target hatte ich nur mal vorsorglich definiert aber dann nicht gebraucht. Den "EmployeeMapped" konnte ich auch nicht adden da mein "Add" Befehl nur eine

EmployeeEntity

akzeptiert. Der Code dazu in verkürzter Form:

    public interface IEmployeeRepository : ISqlRepository<EmployeeEntity>
    {
    }
    public class EmployeeRepository : SqlRepository<EmployeeEntity>, IEmployeeRepository
    {
        public EmployeeRepository(IDbContext dbContext) : base(dbContext)
        {
        }
        protected override string TableName { get; } = "Employees";
        protected override string IdField { get; } = "Id";
    }

und der entsprechende Add Befehl aus dem Repository

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

--> Was funktionieren würde wäre sowas (irgendwie macht das aber keinen Sinn denn dann wäre das Mapping völlig umsonst - zumindest aus meiner Sicht).

                newEmployee.FirstName = EmployeeMapped.FirstName;
                newEmployee.LastName = EmployeeMapped.LastName;
                newEmployee.DepartmentFK = EmployeeMapped.DepartmentFK;
                Sending.Add(newEmployee);

Das Thema Mapping und aus der SQLIte Datei in eine Liste für die View packen habe ich komplett net geschnallt. Dazu habe ich nicht ein schlaues Beispiel gefunden, geschweige denn eins mit SQLite oder so.

Wäre für Hilfe echt dankbar damit ich das Thema zumindest etwas kapiere.

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

(Bitte den letzten Post ignorieren)
Ich hake hier nochmal nach da mir eure beiden Antworten ("vermeide Fehlerquellen mit der Logikschicht" und "bau dir ein eigenes ViewModel und mappe das entsprechend") keine Ruhe lassen. Hab jetzt soweit fast alles in anständiger Form (aus meiner Sicht) und will das nun auch noch hin kriegen!

in der Theorie habe ich folgende Ausgangslage:
Entity 1

public class EmployeeEntity : Entity
    {
        public EmployeeEntity()
        {...}
        public string FirstName
        {...}
        public string LastName
        {...}
        public int DepartmentFK  //Foreign Key zur Tabelle Departments in der SQLite Datei
        {...}
    }

Entity 2

public class DepartmentEntity : Entity
    {
        public string Department
        {...}
    }

für meine View erstelle ich nun ein neues ViewModel das die entsprechenden Properties bereit stellt die ich dann in eine ObservableCollection packen kann (keine Entity kommt hier direkt vor) und die View kann per Binding drauf zugreifen.

public class Combined : Entity
    {
        public string FirstName
        {...}
        public string LastName
        {...}
        public string Department
        {...}
    }

soweit komme ich ja noch mit, allerdings geht es jetzt ja darum die Werte aus der DB zu holen. Bis jetzt ging das via "Schmeiss die Entity in eine generische Dapper Methode und gib mir alle Zeilen raus", sollte man ja nun nicht mehr machen. Wenn ich die Automapper Tutorials richtig verstanden habe kann ich ein Mapping erstellen und dann direkt im Mapping die Resultate der neuen ObservableCollection zuweisen. Das ViewModel File sähe nun in etwa so aus

    public class TestViewModel : Screen
    {
        private readonly IEmployeeRepository _emp;
        private readonly IDepartmentRepository _dep;
        private ObservableCollection<Combined> _test;
        public TestViewModel(IEmployeeRepository Emp, IDepartmentRepository Dep)
        {
            _emp = Emp;
            _dep = Dep;
            var config = new MapperConfiguration(cfg => {
                cfg.CreateMap<EmployeeEntity, Combined>();
                cfg.CreateMap<DepartmentEntity, Combined>().ForMember(dest => dest.DepartmentFK, opt => opt.MapFrom(src => src.Id));
                });
            var mapper = config.CreateMapper();
            var employees = _emp.GetAll().ToObservable();
            _test = mapper.Map(_emp.GetAll().ToObservable(), _test);
            _test = mapper.Map(_dep.GetAll().ToObservable(), _test);
        }

        public ObservableCollection<Combined> Test
        {
            get { return _test; }
            set { _test = value; }
        }
    }
    public class Combined
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int DepartmentFK { get; set; }
        public string Department { get; set; }
    }

Das funktioniert auch, allerdings killt der 2. _test = mapper.Map logischerweise meine Combined Collection. Bin ich hier generell auf dem richtigen Weg oder völlig daneben und Ihr meint etwas ganz anderes?

16.807 Beiträge seit 2008
vor 4 Jahren

Den Mapper erstellt man zentral; kann man hier einfach statisch machen.


public static class MyMapper
{
    private static bool _init;
    public static Init()
    {
        if (!_init)
        {
            Mapper.Initialize(c =>
            {
                c.CreateMap<xxxxxx>();
            });
            _init= true;
        }
    }
}

Das wird dann beim Applikationsstart aufgerfen.

Das funktioniert auch, allerdings killt der 2. _test = mapper.Map logischerweise

Naja; das wundert Dich? Du überschreibst ja auch alles.

Du kannst nicht so ohne weiteres von mehreren Quellen in ein Ziel mappen.
Prinzipiell müsstest Du hier das komplette Mapping manuell konfigurieren; sprich Member für Member und jeweils die Information hinterlegen, wo AutoMapper die Werte der einzelnen Eigenschaften findet.

Combined (bescheuerter Name 😉 nenne ich ma DepartmentEmployee

    var emMap = Mapper.CreateMap<EmployeeEntity,DepartmentEmployee>();
        emMap.ForAllMembers(d => d.Ignore()); 
        emMap.ForMember(e => e.FirstName, opt => opt.MapFrom(s => s.FirstName))
            .ForMember(e => e.LastName, opt => opt.MapFrom(s => s.LastName));

     var deMap = Mapper.CreateMap<DepartmentEntity, DepartmentEmployee>();
        deMap.ForAllMembers(d => d.Ignore());
        deMap.ForMember(d => d.Department, opt => opt.MapFrom(s => s.Department));

Soweit ich weiß musst Du dann aber doppelt mappen - manuell.

var depEm = Mapper.Map<EmployeeEntity,DepartmentEmployee>(employee); // füllt Employee
depEm = Mapper.Map<DepartmentEntity,DepartmentEmployee>(employee); // füllt Department

Bin mir aber sicher, dass sich hier andere, wie AutoMapper und WPF exzessiv verwenden, evtl schlauere Lösungen oder entsprechende Extensions geschrieben haben.

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

Packe den Mapper, wenn denn alles funktioniert, in den Bootstrapper. Hab das aber absichtlich noch nicht gemacht damit ich im Notfall einfach die Datei löschen kann.

Ne klar wundere ich mich nicht 🙂 wichtig war mir gestern nur das zumindest das mit den Employees klappt und das Mapping funktioniert.

Ja ausser Combined fiel mir echt nix besseres ein auf die Schnelle 🙂
Kannst du mir verraten warum du erst "Ignore" machst und dann die Properties zuweist?

Muss nochmal die Doku von Automapper studieren, irgendwie muss das doch klappen

16.807 Beiträge seit 2008
vor 4 Jahren

Kannst du mir verraten warum du erst "Ignore" machst und dann die Properties zuweist?

Prinzipiell müsstest Du hier das komplette Mapping manuell konfigurieren

😉

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

Yep, wer lesen kann ist klar im Vorteil 😃

Mir ist aber immer noch nicht alles klar (leider), vor Allem wie die Befüllung mit Daten stattfinden soll. Ich habe ein anderes Beispiel gefunden

-->https://stackoverflow.com/questions/41368164/automapper-to-join-records-from-2-tables-into-single-ienumerable-viewmodel

Er "joined" 2 Listen und gibt t3 zurück. Wenn ich das nun "Quick und Dirty" darstelle kriege ich für die IEnumerable "result" tatsächlich das Department als Klartext zurück.

        public TestViewModel(IEmployeeRepository Emp, IDepartmentRepository Dep)
        {
            _emp = Emp;
            _dep = Dep;
            var config = new MapperConfiguration(cfg =>
            {
                cfg.CreateMap<EmployeeEntity, Combined>();
                cfg.CreateMap<DepartmentEntity, Combined>();
            });
            var mapper = config.CreateMapper();
            var t1 = new ObservableCollection<EmployeeEntity>(_emp.GetAll().ToObservable());
            var t2 = new ObservableCollection<DepartmentEntity>(_dep.GetAll().ToObservable());

            var result = t1.Join(t2, t => t.DepartmentFK, t => t.Id, (EmployeeEntity, DepartmentEntity) =>
            {
                var t3 = mapper.Map<EmployeeEntity, Combined>(EmployeeEntity);
                t3 = mapper.Map(DepartmentEntity, t3);
                return t3;
            });
            foreach (var item in result)
            {
                MessageBox.Show(item.FirstName);
                MessageBox.Show(item.Department);
            }
        }

Scheint also zu funktionieren, nun stellen sich 2 Fragen:
1.) Ist das so legitim?
2.) einen Teil der Mapper Sachen in die Bootstrapper.cs Datei und den Rest im ViewModel belassen oder in eine "Logik" Datei auslagern?

PS:
Als Alternative kam mir vorhin noch in den Sinn das man ja auch eine separate SQL Abfrage machen könnte und diese dann in die ObservableCollection wirft, komplett ohne Mapping (allerdings "wurmt" mich dieser Ansatz und ich will eigentlich das Ganze per Mapping lösen)

16.807 Beiträge seit 2008
vor 4 Jahren

Du, Du wirst nicht den perfekten Code schreiben - niemals. Den gibt es nicht.

Du solltest es erstmal so machen, dass Du es prinzipiell verstehst.
Legitim oder nicht legitim ist auch oft Anforderungssache....