Reprenons l'exemple précédent et faisons la même chose avec du TPT.
Au niveau du ssdl, il suffit de rajouter ceci :
<EntitySet Name="Managers" EntityType="TestModel1.Store.Managers">
<DefiningQuery>
SELECT DISTINCT EmployeeManager
FROM EmployeesWithManager
</DefiningQuery>
</EntitySet>
et
<EntityType Name="Managers">
<Key>
<PropertyRef Name="EmployeeManager" />
</Key>
<Property Name="EmployeeManager" Type="int" Nullable="false" />
</EntityType>
Ensuite, il suffit de créer l'entity type Manager, le faire hériter de Employee, le mapper sur notre vue ssdl en lui disant que la colonne EmployeeManager est mappée sur EmployeeId.
C'est vraiment trop simple l'Entity Framework 
Ce post s'inspire d'un post précédent "EF : Comment faire de l'Entity Splitting avec des PK différentes ?"
L'idée ici est la suivante. On a une table Employee avec 3 colonnes : EmployeeId (PK), EmployeeName et EmployeeManager (FK vers EmployeeId).
De là, je voudrais générer avec EDM deux entity types : Employee et Manager avec Manager héritant d'Employee.
Comment faire cela ?
L'idée est d'utiliser une ssdl view pour faire du TPH.
Il suffit de rajouter le code suivant dans le ssdl :
<EntitySet Name="EmployeeWithManagerStatus" EntityType="TestModel.Store.EmployeeWithManagerStatus">
<DefiningQuery>
SELECT EWM2.EmployeeId, CAST(CASE(EWM2.SubEmployeesCount) WHEN 0 THEN 0 ELSE 1 END AS BIT) AS IsManager
FROM
(SELECT EmployeeId, (
SELECT count(1)
FROM EmployeesWithManager
WHERE EmployeeManager = EWM1.EmployeeId) AS SubEmployeesCount
FROM EmployeesWithManager AS EWM1) AS EWM2
</DefiningQuery>
</EntitySet>
<EntityType Name="EmployeeWithManagerStatus">
<Key>
<PropertyRef Name="EmployeeId" />
</Key>
<Property Name="EmployeeId" Type="int" Nullable="false" />
<Property Name="IsManager" Type="bit" />
</EntityType>
A ce stade, tout le travail est quasiment fait. Il suffit alors de rajouter une entité Manager, de rajouter l'héritage entre Manager et Employee puis de mapper l'entity type Manager sur notre vue ssdl. En dernier lieu, il suffit de rajouter une condition sur le mapping en précisant que pour les Managers, la colonne IsManager = true.
Et voilà, le tour est joué. Sympa non ?
Pour ceux qui comme moi n'ont pas eu la chance d'assister au mercredi du dev sur le parallel framework, mais aussi pour les autres,
les webcasts sont en ligne
Imaginons que l'on veuille écrire une application dont le but est de gérer les stocks de produits en se basant sur Northwind sans avoir à les créer.
Dans cette optique, on souhaite avoir deux Entitysets : Product et Supplier avec Supplier en ReadOnly.
On aimerait également avoir une propriété CategoryName ReadOnly dans Product.
Comment faire cela avec un minimum de code ?
Pour commencer, il faut passer tous les set de Supplier à private. Pour cela, on modifiera l'attribut xml Setter.
Bien entendu, l'affectation du Setter à Private peut se faire via la fenêtre de properiétés du designer d'EDM (depuis VS 2008 SP1 Beta).
Ensuite, on va passer le set de la propriété Supplier de Product à private et passer la propriété (get + set) Products de Supplier à private.
On va également supprimer la propriété CategoryID de Product.
A ce niveau là, votre csdl doit ressembler à ça :
<!-- CSDL content -->
<edmx:ConceptualModels>
<Schema Namespace="NorthwindEFModel" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
<EntityContainer Name="NorthwindEFEntities">
<EntitySet Name="Products" EntityType="NorthwindEFModel.Product" />
<EntitySet Name="Suppliers" EntityType="NorthwindEFModel.Supplier" />
<AssociationSet Name="FK_Products_Suppliers" Association="NorthwindEFModel.FK_Products_Suppliers">
<End Role="Suppliers" EntitySet="Suppliers" />
<End Role="Products" EntitySet="Products" />
</AssociationSet>
</EntityContainer>
<EntityType Name="Product">
<Key>
<PropertyRef Name="ProductID" />
</Key>
<Property Name="ProductID" Type="Int32" Nullable="false" />
<Property Name="ProductName" Type="String" Nullable="false" MaxLength="40" Unicode="true" FixedLength="false" />
<Property Name="QuantityPerUnit" Type="String" MaxLength="20" Unicode="true" FixedLength="false" />
<Property Name="UnitPrice" Type="Decimal" Precision="19" Scale="4" />
<Property Name="UnitsInStock" Type="Int16" />
<Property Name="UnitsOnOrder" Type="Int16" />
<Property Name="ReorderLevel" Type="Int16" />
<Property Name="Discontinued" Type="Boolean" Nullable="false" />
<NavigationProperty Name="Supplier" Relationship="NorthwindEFModel.FK_Products_Suppliers" FromRole="Products" ToRole="Suppliers" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
</EntityType>
<EntityType Name="Supplier">
<Key>
<PropertyRef Name="SupplierID" />
</Key>
<Property Name="SupplierID" Type="Int32" Nullable="false" />
<Property Name="CompanyName" Type="String" Nullable="false" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="ContactName" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="ContactTitle" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="Address" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="City" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="Region" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="PostalCode" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="Country" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="Phone" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="Fax" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<Property Name="HomePage" Type="String" Nullable="true" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" />
<NavigationProperty Name="Products" Relationship="NorthwindEFModel.FK_Products_Suppliers" FromRole="Suppliers" ToRole="Products" a:SetterAccess="Private" xmlns:a="http://schemas.microsoft.com/ado/2006/04/codegeneration" a:GetterAccess="Private" />
</EntityType>
<Association Name="FK_Products_Suppliers">
<End Type="NorthwindEFModel.Supplier" Role="Suppliers" Multiplicity="0..1" />
<End Type="NorthwindEFModel.Product" Role="Products" Multiplicity="*" />
</Association>
</Schema>
</edmx:ConceptualModels>
Il nous reste maintenant à rajouter notre propriété CategoryName sur Product.
Comment faire cela. On peut bien sûr utiliser une vue à la place de la table Products et des procédures stockées ou même définir une vue ssdl avec des fonctions ssdl pour les opérations CUD (pour plus d'infos, voir mon article sur EDM). Cependant, nous sommes partisant du moindre effort ("Comment faire cela avec un minimum de code ?"). Pour cela, nous allons définir une vue ssdl réduite et nous allons utiliser l'EntitySplitting pour garder la partie de Product déjà générée par le designer. De plus cette approche nous évitera de coder les functions ssdl vu que l'on suppose que la propriété CategoryName de Product est en ReadOnly.
Pour cela, dans le SSDL, on va définir un nouveau EntitySet :
<EntitySet Name="ProductCategoryName" EntityType="NorthwindEFModel.Store.ProductCategoryName">
<DefiningQuery>
SELECT P.ProductID, C.CategoryName
FROM Products AS P
INNER JOIN Categories as C ON C.CategoryID = P.CategoryID
</DefiningQuery>
</EntitySet>
et un nouveau EntityType (toujours dans le SSDL) :
<EntityType Name="ProductCategoryName">
<Key>
<PropertyRef Name="ProductID" />
</Key>
<Property Name="ProductID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
<Property Name="CategoryName" Type="nvarchar" MaxLength="15" />
</EntityType>
Maintenant, on peut revenir en mode design et ajouter la propriété CategoryName (string avec setter à Private) et faire de l'EntitySplitting sur Product.

Et voila, c'est (déjà) fini. Vous avez un EntityType Supplier ReadOnly et un EntityType Product avec une propriété CategoryName ReadOnly.
L'extension method AsEnumerable permet de passer de IQueryable<T> à IEnumerable<T>. Cela permet par exemple d'appeler dans une requêtes LINQ To SQL (ou LINQ To Entities) des méthodes non gérées par LINQ To SQL/Entities.
Cependant, cette méthode peut avoir d'autres intérêts.
Imaginons que l'on veuille récupérer tous les Orders (de Northwind) dont la date serait supérieur ou égale à 1998 avec leur propriété Customer rempli si et seulement si le pays est la france avec l'Entity Framework (sans Lazy Loading).
Pour cela, il faut charger dans le contexte les customers qui vont bien et retourner les orders qui vont bien.
Comment faire cela en une seule requête LINQ ?
On pourrait imaginer que ce code marche :
from oc in
from o in context.Orders
where o.OrderDate.HasValue && o.OrderDate.Value.Year >= 1998
select new { Order = o, Customer = context.Customers.Where(c => c.CustomerID == o.Customers.CustomerID && c.Country == "FRANCE").FirstOrDefault() }
select oc.Order;
Mais non. En effet, la requête SQL ne récupérant au final que les Orders, les Customers ne seront pas chargés dans le contexte.
La requête SQL générée sera la suivante :
SELECT
1 AS [C1],
[Filter1].[OrderID] AS [OrderID],
[Filter1].[EmployeeID] AS [EmployeeID],
[Filter1].[OrderDate] AS [OrderDate],
[Filter1].[RequiredDate] AS [RequiredDate],
[Filter1].[ShippedDate] AS [ShippedDate],
[Filter1].[ShipVia] AS [ShipVia],
[Filter1].[Freight] AS [Freight],
[Filter1].[ShipName] AS [ShipName],
[Filter1].[ShipAddress] AS [ShipAddress],
[Filter1].[ShipCity] AS [ShipCity],
[Filter1].[ShipRegion] AS [ShipRegion],
[Filter1].[ShipPostalCode] AS [ShipPostalCode],
[Filter1].[ShipCountry] AS [ShipCountry],
[Filter1].[CustomerID] AS [CustomerID]
FROM (SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry]
FROM [dbo].[Orders] AS [Extent1]
WHERE ([Extent1].[OrderDate] IS NOT NULL) AND ((DATEPART (year, [Extent1].[OrderDate])) >= 1998) ) AS [Filter1]
OUTER APPLY (SELECT TOP (1) [Extent2].[CustomerID] AS [CustomerID], [Extent2].[CompanyName] AS [CompanyName], [Extent2].[ContactName] AS [ContactName], [Extent2].[ContactTitle] AS [ContactTitle], [Extent2].[Address] AS [Address], [Extent2].[City] AS [City], [Extent2].[Region] AS [Region], [Extent2].[PostalCode] AS [PostalCode], [Extent2].[Country] AS [Country], [Extent2].[Phone] AS [Phone], [Extent2].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent2]
WHERE ([Extent2].[CustomerID] = [Filter1].[CustomerID]) AND (N'FRANCE' = [Extent2].[Country]) ) AS [Limit1]
Notons qu'après l'optimisation de SQL Server, cette requête a le même plan d'exécution que celle-ci :
SELECT
1 AS C1,
OrderID,
EmployeeID,
OrderDate,
RequiredDate,
ShippedDate,
ShipVia,
Freight,
ShipName,
ShipAddress,
ShipCity,
ShipRegion,
ShipPostalCode,
ShipCountry,
CustomerID
FROM Orders
WHERE (OrderDate IS NOT NULL) AND ((DATEPART (year, OrderDate)) >= 1998)
En revanche, si on utilise cette requête LINQ :
from oc in
(from o in context.Orders
where o.OrderDate.HasValue && o.OrderDate.Value.Year >= 1998
select new { Order = o, Customer = context.Customers.Where(c => c.CustomerID == o.Customers.CustomerID && c.Country == "FRANCE").FirstOrDefault() }
).AsEnumerable()
select oc.Order;
la requête SQL va ramener les orders qui vont bien mais également les customers qui vont bien :
SELECT
1 AS [C1],
1 AS [C2],
[Filter1].[OrderID] AS [OrderID],
[Filter1].[EmployeeID] AS [EmployeeID],
[Filter1].[OrderDate] AS [OrderDate],
[Filter1].[RequiredDate] AS [RequiredDate],
[Filter1].[ShippedDate] AS [ShippedDate],
[Filter1].[ShipVia] AS [ShipVia],
[Filter1].[Freight] AS [Freight],
[Filter1].[ShipName] AS [ShipName],
[Filter1].[ShipAddress] AS [ShipAddress],
[Filter1].[ShipCity] AS [ShipCity],
[Filter1].[ShipRegion] AS [ShipRegion],
[Filter1].[ShipPostalCode] AS [ShipPostalCode],
[Filter1].[ShipCountry] AS [ShipCountry],
[Filter1].[CustomerID] AS [CustomerID],
[Limit1].[CustomerID] AS [CustomerID1],
[Limit1].[CompanyName] AS [CompanyName],
[Limit1].[ContactName] AS [ContactName],
[Limit1].[ContactTitle] AS [ContactTitle],
[Limit1].[Address] AS [Address],
[Limit1].[City] AS [City],
[Limit1].[Region] AS [Region],
[Limit1].[PostalCode] AS [PostalCode],
[Limit1].[Country] AS [Country],
[Limit1].[Phone] AS [Phone],
[Limit1].[Fax] AS [Fax]
FROM (SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry]
FROM [dbo].[Orders] AS [Extent1]
WHERE ([Extent1].[OrderDate] IS NOT NULL) AND ((DATEPART (year, [Extent1].[OrderDate])) >= 1998) ) AS [Filter1]
OUTER APPLY (SELECT TOP (1) [Extent2].[CustomerID] AS [CustomerID], [Extent2].[CompanyName] AS [CompanyName], [Extent2].[ContactName] AS [ContactName], [Extent2].[ContactTitle] AS [ContactTitle], [Extent2].[Address] AS [Address], [Extent2].[City] AS [City], [Extent2].[Region] AS [Region], [Extent2].[PostalCode] AS [PostalCode], [Extent2].[Country] AS [Country], [Extent2].[Phone] AS [Phone], [Extent2].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent2]
WHERE ([Extent2].[CustomerID] = [Filter1].[CustomerID]) AND (N'FRANCE' = [Extent2].[Country]) ) AS [Limit1]
ceux-ci seront donc présents dans le contexte nous permettant d'arriver au résultat souhaité.
Tout comme Julie qui a également blogué dessus, j'ai remarqué la réponse de Daniel Simmons à Ben S et tout comme Julie, je n'avais jamais utilisé cela par le passé.
L'idée est la suivante :
A partir de Northwind, on a une catégorie. On veut charger les produits relatifs à cette catégorie. Jusque là pas de difficulté :
category.Products.Load();
Maintenant, imaginons que l'on veuille charger les produits avec leur fournisseur.
La méthode Load ne retourne rien donc pas de méthode Include possible.
La réponse de Daniel (en attendant peut-être mieux dans la V2) consiste à faire ceci :
category.Products.Attach(category.Products.CreateSourceQuery().Include("Supplier"));
J'ai récemment fait un post pour remplacer la méthode Include de la classe ObjectQuery<T> par une extension method qui prendrait un Func en argument à la place d'un string, avec tous les avantages que cela implique : Intellisence, code vérifié à la compilation et non à l'exécution.
Sur mon blog anglais, j'ai eu un commentaire qui m'a amené à réfléchir.
Celui qui m'a mis ce commentaire voudrais un Include qui inclut une condition.
Je pense qu'il serait particulièrement intéressant de modifier la génération de l'Expression Tree afin de permettre ce cas mais, souhaitant lui répondre rapidement, ce n'est pas la solution que j'ai envisagé. En fait, pour répondre à sa problèmatique, c'est très simple, il suffit de charger dans le contexte uniquement les entités qu'ils souhaitent (à condition que les autres entités ne soient pas déjà présentes).
Le mieux, serait de retourner à la fois l'entité parente et les entités filles souhaitées.
Prenons un ex : Northwind avec les table Customers et Orders.
from c in context.Customers
select new { Customer = c, Orders = c.Orders.Where(o => o.OrderDate.HasValue && o.OrderDate.Value.Year >= 1998) }
Cette solution n'était cependant pas satisfaisante dans la mesure où il n'est pas possible de retourner des types anonymes dans un service WCF. Il faut donc retourner les customers liés aux Orders qui vont bien.
Après en avoir discuté avec Daniel Simmons, je vous propose la solution suivante :
Etendre (partial) la classe Customer comme ceci :
partial class Customer
{
public Customer AttachOrders(IEnumerable<Order> orders)
{
Orders.Attach(orders);
return this;
}
}
Il me suffit ensuite de modifier ma requête LINQ comme ça :
from c2 in
(from c in context.Customers
select new { Customer = c, Orders = c.Orders.Where(o => o.OrderDate.HasValue && o.OrderDate.Value.Year >= 1998) }).AsEnumerable()
select c2.Customer.AttachOrders(c2.Orders);
Par contre, attention à bien utiliser un contexte vierge pour faire cela. En effet, si vous aviez chargé des Orders antérieures à 1998, vous les aurez avec votre Customer.
Si vous êtes dispo le 23 juin pm (ce que n'est malheureusement pas mon cas), je ne peux que vous conseiller la presentation (en anglais) du parallel framework chez MS à Paris.
Pour plus d'infos ou pour vous inscrire, c'est ici.
Imaginons le cas suivant :
public class MyBusinessClass
{
public MyEnum Value { get; set; }
}
public enum MyEnum
{
One,
Two,
Three,
Four,
Five
}
Je veux générer mes radios buttons à la volée et les bindés sur ma classe métier.
Problème : Comment binder chacun de mes radios buttons sur ma classe métier ?
- Il me faudrait une propriété par valeur d'enum pour savoir si je dois cocher ou non mon radio button.
Horrible, je ne vais pas pourrir l'interface de ma couche BLL à cause de mon UI.
- Je n'ai qu'à créer un proxy dans ma couche UI qui va encapsuler mon objet métier et rajouter les propriétés qui vont bien.
Mouais, il y a du mieux mais si mon enum ou, plus probable, ma classe MyBusinessClass évolue, mon code n'est pas dynamique, il me faudra rajouter les propriétés. Cette solution manque donc clairement de souplesse.
- Je vais garder cette idée de proxy mais plutôt que de recréer les propriétés, je vais utiliser des PropertyDescriptor.
public class MyUIBusinessProxy<BusinessType> : ICustomTypeDescriptor
{
public MyUIBusinessProxy(BusinessType myBusinessObject)
{
MyBusinessObject = myBusinessObject;
}
public BusinessType MyBusinessObject { get; private set; }
#region ICustomTypeDescriptor Members
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(MyBusinessObject);
}
string ICustomTypeDescriptor.GetClassName()
{
return TypeDescriptor.GetClassName(MyBusinessObject);
}
string ICustomTypeDescriptor.GetComponentName()
{
return TypeDescriptor.GetComponentName(MyBusinessObject);
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
return TypeDescriptor.GetConverter(MyBusinessObject);
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(MyBusinessObject);
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(MyBusinessObject);
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(MyBusinessObject, editorBaseType);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(MyBusinessObject, attributes);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return TypeDescriptor.GetEvents(MyBusinessObject);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
return GetPropertyDescriptors(attributes);
}
private PropertyDescriptorCollection GetPropertyDescriptors(Attribute[] attributes)
{
var propsColl = TypeDescriptor.GetProperties(MyBusinessObject, attributes, true);
var props = new List<PropertyDescriptor>();
foreach (PropertyDescriptor prop in propsColl)
{
props.Add(prop);
if (prop.PropertyType.IsEnum)
{
var enumValues = Enum.GetValues(prop.PropertyType);
var enumNames = Enum.GetNames(prop.PropertyType);
for (int index = 0; index < enumValues.Length; index++)
props.Add(new IsEnumValuePropertyDescriptor(MyBusinessObject, prop.Name, prop.PropertyType, attributes, enumNames[index], (int)enumValues.GetValue(index)));
}
}
return new PropertyDescriptorCollection(props.ToArray());
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return GetPropertyDescriptors(null);
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return MyBusinessObject;
}
#endregion ICustomTypeDescriptor Members
private class IsEnumValuePropertyDescriptor : PropertyDescriptor
{
private BusinessType _businessObject;
private string _propertyName;
private Type _propertyType;
private int _value;
public IsEnumValuePropertyDescriptor(BusinessType businessObject, string propertyName, Type propertyType, Attribute[] attr, string valueName, int value)
: base (propertyName + "Is" + valueName, attr)
{
_businessObject = businessObject;
_propertyName = propertyName;
_propertyType = propertyType;
_value = value;
}
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { return typeof(BusinessType); }
}
public override object GetValue(object component)
{
return (int)(typeof(BusinessType).GetProperty(_propertyName).GetValue(_businessObject, null)) == _value;
}
public override bool IsReadOnly
{
get { return false; }
}
public override Type PropertyType
{
get { return typeof(bool); }
}
public override void ResetValue(object component)
{
}
public override void SetValue(object component, object value)
{
if ((bool)value)
typeof(BusinessType).GetProperty(_propertyName).SetValue(_businessObject, _value, null);
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
}
}
Maintenant, je peux donc facilement faire :
foreach (var enumName in Enum.GetNames(typeof(MyEnum)))
{
var rb = new RadioButton();
myFlowLayoutPanel.Controls.Add(rb);
rb.Text = enumName;
rb.DataBindings.Add("Checked", myUIBusinessProxy, "ValueIs" + enumName);
}
Krzysztof Cwalina
a annoncé
la première CTP public de Managed Extensibility Framework (MEF)
.
Pour faire simple, MEF est un framework qui permet de faciliter la gestion des add-in/plug-in dans vos applications.
Enjoy :-)
J'ai défini un Include qui prend un Func en paramètre.
Cool. Mais le problème c'est que je suis limité aux relations avec un seul niveau. En effet, je ne sais pas traiter Include("Products.Order_Details")?
J'ai donc modifier mon code comme ceci :
public static class ObjectQueryExtension
{
public static ObjectQuery<T> Include<T>(this ObjectQuery<T> mainQuery, Expression<Func<T, object>> subSelector)
{
return mainQuery.Include(FuncToString(subSelector.Body));
}
private static string FuncToString(Expression selector)
{
switch (selector.NodeType)
{
case ExpressionType.MemberAccess:
return ((selector as MemberExpression).Member as Reflection.PropertyInfo).Name;
case ExpressionType.Call:
var method = selector as MethodCallExpression;
return FuncToString(method.Arguments[0]) + "." + FuncToString(method.Arguments[1]);
case ExpressionType.Quote:
return FuncToString(((selector as UnaryExpression).Operand as LambdaExpression).Body);
}
throw new InvalidOperationException();
}
public static K Include<T, K>(this EntityCollection<T> mainQuery, Expression<Func<T, object>> subSelector)
where T : EntityObject, IEntityWithRelationships
where K : class
{
return null;
}
public static K Include<T, K>(this T mainQuery, Expression<Func<T, object>> subSelector)
where T : EntityObject
where K : class
{
return null;
}
}
et je peux maintenant faire ça :
context.Categories.Include(ca => ca.Products.Include<Products, Order_Details>(p => p.Order_Details).Include<Order_Details, Orders>(od => od.Orders))
Si vous voulez charger les catégories avec les produits, vous allez utiliser la méthode Include:
context.Categories.Include("Products")
Mais ce que je trouve vraiment nul (et je suis pas le seul), c'est qu'il faille utiliser Products sous la forme d'un string. C'est encore plus choquant de devoir faire ça dans une requête LINQ.
J'ai donc défini une extension method qui permet de saisir un Func à la place d'un string :
public static class ObjectQueryExtension
{
public static ObjectQuery<T> Include<T>(this ObjectQuery<T> mainQuery, Expression<Func<T, object>> subSelector)
{
return mainQuery.Include(((subSelector.Body as MemberExpression).Member as Reflection.PropertyInfo).Name);
}
}
Et je peux maintenant faire:
context.Categories.Include(c => c.Products)
ce que je trouve vraiment mieux.
J'ai eu une question qui peut en intéresser plus d'un. Aussi, j'en profite pour bloguer dessus.
Le code suivant :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
var p = new Products { ProductName = "test", Categories = c };
Console.WriteLine(c.Products.Count);
}
attache automatiquement p au contexte. Pourquoi ?
Comme vous le savez probablement si vous avez un peu regardé l'Entity Framework, il n'y a pas de Lazy Loading. Donc quand vous faites p.Categories, il va vous retourner la catégorie présente dans le contexte. Si celle-ci n'existe pas dans le contexte, il va retourner null.
Par conséquent, quand vous affectez une catégorie (présente dans un contexte) à un nouveau produit, le produit sera automatiquement attaché à son tour.
Si vous ne voulez pas de ce fonctionnement, il vous faudra utiliser l'EntityReference :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
var p = new Products { ProductName = "test" };
p.CategoriesReference.EntityKey = c.EntityKey;
}
Le fait d'être attaché au contexte fait que le code suivant :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
var p = new Products { ProductName = "test", Categories = c };
Console.WriteLine(c.Products.Count);
p.Categories = context.Categories.First(categ => categ.CategoryID == 2);
Console.WriteLine(c.Products.Count);
}
retournera 1 puis 0.
Maintenant prenons le code suivant :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
var p = new Products { ProductName = "test", Categories = c };
context.SaveChanges();
}
Ce qui peut quelque peu paraître surprenant c'est que le code suivant va ajouter notre nouveau produit en base alors que nous n'avons pas appelé de Add sur le contexte (ni context.AddToProduct, ni context.AddObject). En fait, en plus de l'attachement de l'entité au contexte, son EntityState va passer de Detached à Added, d'où l'ajout lors du SaveChanges.
Maintenant, creusons un peu cela.
Supposons que l'on supprime le ChangeTracker sur c avant de la lier à p :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
(c as IEntityWithChangeTracker).SetChangeTracker(null);
var p = new Products { ProductName = "test"};
p.Categories = c;
context.SaveChanges();
}
Quand on supprime le ChangeTracker, cela signifie que l'on va pouvoir attacher c à un autre contexte mais tant qu'on ne l'a pas fait, l'EntityChangeTracker de l'entité reste l'ancienne :
private IEntityChangeTracker EntityChangeTracker
{
get
{
if (this._entityChangeTracker == null)
{
this._entityChangeTracker = s_detachedEntityChangeTracker;
}
return this._entityChangeTracker;
}
set
{
this._entityChangeTracker = value;
}
}
Donc dans le cas présent, il y aura un nouvel enregistrement en base.
Maintenant que se passe-t-il dans ce cas là :
using (var context = new NorthwindEntities())
{
var c = context.Categories.First(categ => categ.CategoryID == 1);
(c as IEntityWithChangeTracker).SetChangeTracker(null);
var p = new Products { ProductName = "test"};
using (var context2 = new NorthwindEntities())
{
context2.Attach(c);
p.Categories = c;
}
var c2 = context.Categories.First(categ => categ.CategoryID == 1);<