Immer mehr erweckt sich mir der Eindruck, das das Entity Framework für den Einsatz in Silverlight kein Produktivitätsgewinn ist. Ich möchte das hier am Beispiel eines einfachen LOB Prototypen zeigen. Die Service Schicht ist mit WCF ausgeführt. Ausnahmsweise mein Beispiel mit C# und nicht mit VB.NET. Datenbank ist Northwind. Das erzeugen eines EF Datamodels aus der Customer Tabelle setze ich als gegeben voraus. Erstellt wird ein Silverlight enabled WCF Service. Im Silverlight Projekt wird eine Service Reference auf den WCF Server erstellt. Alle Namen sind gewählt wie vom Visual Studio vorgeschlagen (model1.edmx, service1.svc). Service Code ist gelb hinterlegt.
Select
Für das Laden der Daten wird eine Methode im WCF Service erstellt die eine Liste von Kunden zurückgibt.
[OperationContract]
public List<Customers> holeKunden()
{
NorthwindEntities ne = new NorthwindEntities();
return ne.Customers.ToList();
}
In der Silverlight Business Anwendung werden ein Datagrid und drei Buttons eingefügt für Select, Insert und Delete. Auf der Instanz des WCF ServiceClient wird dann die Methode für SELECT holekunden asynchron aufgerufen.
ServiceReference1.Service1Client svc = new ServiceReference1.Service1Client();
private void button1_Click(object sender, RoutedEventArgs e)
{
svc.holeKundenCompleted +=
new EventHandler<ServiceReference1.holeKundenCompletedEventArgs>(svc_holeKundenCompleted);
svc.holeKundenAsync();
}
Wenn die Daten geladen sind wird das Datagrid befüllt.
void svc_holeKundenCompleted(object sender, ServiceReference1.holeKundenCompletedEventArgs e)
{
dataGrid1.ItemsSource = e.Result;
}
So weit so gut.
Insert
Im Service funktioniert das einfügen eines neuen Records (hier Entiät) per AddObject und anschliessendem SaveChanges. Das komplette Customer Objekt wird von Silverlight als Parameter übergeben.
[OperationContract]
public bool InsertKunden(Customers cs)
{
NorthwindEntities ne = new NorthwindEntities();
ne.Customers.AddObject(cs);
ne.SaveChanges();
return true;
}
Da das Datagrid keine Daten direkt einfügen kann, habe ich ein Childwindow erzeugt, mit dem der Benutzer den kompletten neuen Record anlegen kann.
Dem Childwindow wird ein neues leeres Customer Objekt übergeben.
private void button2_Click(object sender, RoutedEventArgs e) {
customerinsert ci = new customerinsert();
ci.DataContext=new ServiceReference1.Customers();
ci.Unloaded += new RoutedEventHandler(ci_Unloaded);
ci.Show();
}
Auch hier läuft es asynchron. Wenn also das Childwindow geschlossen wurde, wird der Datacontext zurückgerettet und in Kunden gecastet. Der Service (InsertKunden) wird aufgerufen und das neue Object als Parameter übergeben.
void ci_Unloaded(object sender, RoutedEventArgs e)
{
var ci= (customerinsert)sender;
if ((bool)ci.DialogResult)
{
((ObservableCollection <ServiceReference1.Customers>) dataGrid1.ItemsSource).Add
((ServiceReference1.Customers) (ci).DataContext);
svc.InsertKundenAsync((ServiceReference1.Customers) (ci).DataContext);
}
}
Im Childwindow ist kein Code. Die Daten werden rein per Binding Deklaration im XAML eingebunden.
Das Formular selbst enthält ein Grid Element und Textboxen.
<sdk:Label Content="kundennummer:" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Height="23"
HorizontalAlignment="Left" Margin="3" Name="customerIDTextBox"
Text="{Binding Path=CustomerID, Mode=TwoWay}" VerticalAlignment="Center"
Width="120" />
Allerdings muss man sich selbst um die Aktualisierung des Datagrids kümmern und den neuen Datensatz anhängen. Deswegen die Zeile mit dem Cast nach ObservableCollection. Dies ist der Standardtyp der beim erzeugen einer ServiceReferenz in Visual Studio für generische Listen genommen wird.
Schon nicht mehr ganz so nett.
Delete
Wenn man in einem ORM System eine Entity löschen will, muss man diese Entität besitzen. Das ist in einem entkoppelten statuslosen mehrschichtigen System nicht der Fall. WCF wird sessionless betrieben und verliert nach jedem Request die Daten und damit auch die Entity für den zu löschenden Satz. Man müsste nun die Entity wieder im Service neu laden um sie dann zu löschen. Das halte ich für Absurd. Deswegen nehme ich den good old way über SQL und übertrage nur die ID des zu löschenden Datensatzes. Im Update Fall werde ich noch näher auf die Probleme mit EF eingehen. Mann kann auf einer Entity Connection Commands ausführen. Die Sprache ist zwar extrem TSQL ähnlich heißt aber ESQL. Man sieht das hier im Code an der Verwendung eines parametrisierten Commands.
public bool deleteKunden(string id)
{
NorthwindEntities ne = new NorthwindEntities();
//injection save
var ret= ne.ExecuteStoreCommand(@"delete from customers where customerid={0}",id);
if (ret==1)
{
return true;
}
return false ;
}
Theoretisch sind die commands mit der @ Parameter Syntax möglich. In jedem Falle ist das SQL Injection sicher. Die Lösung ist Ok, allerdings nicht ganz im reinem Sinne von OR Mapping.
In der Silverlight Business Anwendung wird noch eine Sicherheitsabfrage zwischen geschoben, bevor die Delete Funktion des Services aufgerufen wird.
private void button3_Click(object sender, RoutedEventArgs e)
{
string id = ((ServiceReference1.Customers)dataGrid1.SelectedItem).CustomerID;
if (MessageBox.Show(id + " wirklich löschen?", "Kunden Löschen",
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
svc.deleteKundenAsync(id);
((ObservableCollection<ServiceReference1.Customers>)dataGrid1.ItemsSource).Remove
(((ServiceReference1.Customers)dataGrid1.SelectedItem));
}
}
Auch hier muss ich leider mit einer extra Zeile Code dem Datagrid den gelöschten Datensatz nehmen.
Update
Für das Update steigen wir noch ein wenig tiefer in die Feinheiten von EF ein. Wie vorher schon ausgeführt, kann man keine Entität updaten, die man nicht besitzt. Da WCF normalerweise Sessionless arbeitet zerfallen alle Objekte zu staub. In jedem Fall (auch bei Statefull) wird eine am Silverlight Client geänderte Entität Detached und müsste wieder angehängt (Attach) werden, bevor ein SaveChanges erfolg hat. Da wir aber nichts haben, müssen wir den Object Context in EF ohnehin komplett neu erzeugen. So muss mindestens der Datensatz neu geladen werden der geändert werden soll. So ganz absurd ist das nicht, immerhin könnte ja jemand anders die Daten in der Zwischenzeit auch verändert haben.
[OperationContract]
public bool updateKunden(Customers cs)
{
NorthwindEntities ne = new NorthwindEntities();
Customers temp = ne.Customers.First(x => x.CustomerID == cs.CustomerID);
ne.ApplyCurrentValues(cs.EntityKey.EntitySetName, cs);
ne.SaveChanges();
return true;
}
So werden dann die geänderten Werte des Customers per ApplyCurrentchanges angewendet und dann die Änderungen gespeichert. Interessant ist der Blick auf den SQL Profiler. Zuerst wird ein Datensatz gelesen.
Gespeichert wird nur das veränderte Feld im Datensatz.
Allerdings funtkioniert auch eine Methode die ich als Fake Entity bezeichnen würde. Dann wird der Round Trip zum SQL Server nicht ausgeführt.
Customers temp = new Customers();
temp.CustomerID = cs.CustomerID;
ne.Customers.Attach(temp);
ne.ApplyCurrentValues(cs.EntityKey.EntitySetName, cs);
ne.SaveChanges();
Im Silverlight Client wird das Update nicht per Button ausgelöst, sondern wenn der User im Datagrid die Zeile editiert und dann verlässt. Das passende Event RowEditEnded.
<sdk:DataGrid Height="160" HorizontalAlignment="Left"
RowEditEnded="dataGrid1_RowEditEnded"
Margin="12,47,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="376" />
Der Code ist dazu ganz einfach. Der aktuelle Datensatz befindet sich in den Argumenten des Events und muss noch aus Datacontext in den Customer umgewandelt werden.
private void dataGrid1_RowEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
ServiceReference1.Customers cs = (ServiceReference1.Customers)e.Row.DataContext;
svc.updateKundenAsync(cs);
}
Ich betrachte das schon als sehr gut optimierte Lösung und ein einfaches Szenario. Der Einsatz von Entity Framework erfordert aber zusätzlichen kodierungs Aufwand, der mit z.B. RIA Services deutlich sinkt.