Snabbare/enklare utveckling med Webfoundation (RAD) • Förslag som med framgång redan har smygits in i en del kund projekt, testats och givit snabbare utveckling. – Konfigurerbar model(Mapper) med alternativt factory lager. – Mapper som alternativt Factory lager – Kontrollerbaserad MVC – Autoregistrering i Autofac – Använda standard MVC i största utsträckning eller open source alternativ, t.ex. form validation,js,css minifiering etc. • En fördel med dessa tekniker är att man kan smyga in dessa tekniker i nyutveckling på befintliga WF projekt.
Konfigurerbar produkt modell Vad • Konfigurerbart kunna välja vilken data man vill ha ut i vyn för produkter. • T.ex. kan utvecklare eller kund (med viss risk ) konfigurerbart välja vilken data som behövs i vyn för en produkt sida eller produktlistning eller relaterade produkter, carten etc. • Borde utökas för att klara alla Enova objekt som adress mm.
Konfigurerbar produkt modell Fördelar • Snabbare/enklare utveckling (slipper överlagra faktories, modeller och vill du t.ex. läsa av pris finns det troligen redan en klass för detta) • Går att anpassa model strukturen mer då det är mer otypat (lugn i en vy kan man enkelt ändra). • Bättre prestanda då allt automatiskt cachas och cachningen går att konfigurerbart kund anpassa. (T.ex. kan man välja att inte cacha pris men produkt namn beroende på språk. Vidare så riskerar man inte att läsa av properties som inte behövs för specifik kund. • Enklare att återanvända då kund specifika properties separeras ut i en egen klass. • Möjliggör att en specifik sida som t.ex. en produktlistning kan innehålla olika properties beroende på produkt. (Förutom att konfigurera önskade properties kan man programmatiskt välja properties) • Oberoende av CMS.
ImageMapper ProductlistController RenderAction Hjälp jag behöver html’en för produktlistningen CmsProductlistController Vy och model för CMS’en ProductlistService Aktuella Enova produkter ProductlistMapper Model för produktlistningen (Factory) ProductConfigMapper Hjälp ge mig modellen för varje produkt i produktlistningen med de properties som är konfigurerade Otypad (Dictionary) Model (ProductConfigViewModel) för varje produkt ProductConfigurationService UrlMapper PriceMapper EnovaPropertyMapper Dessa properties skall läsas av och de skall läsas av t.ex. på bara produkten och/eller varianter etc. CMS Produktlistningssida Produkt x1, pris,lager Produkt x2, pris,lager Produkt x3, pris,lager Produkt x4, pris,lager Produkt x5, pris,lager Produkt x6, pris,lager Produkt x7, pris,lager Produkt kategori x LookupVaryByProductService Dessa produkter skall du läsa av properties på Property värdet För tillfället konfigureras allt i en xml fil (runtime settings). Men alternativa servicar som läser från Backoffice kommer/kan enkelt skapas. Man kan enkelt lägga till egna kund anpassade property mappers. EnovaPropertyMapper kan läsa vilken property som hellst som finns på enova objektet medan t.ex. PriceMapper bara kan läsa av price. Konfigurerbar produkt modell flödet för t.ex. en produktlistning.
Konfigurerbar produkt modell Property mapper • INamedPropertyMapper används för mappers som klarar av en specifik property som t.ex. pris. • IPropertyMapper klarar av properties beroende på produkt eller konfigurering. T.ex. Enova properties, bild urler till olika storlekar som konfigureras med ett namn. • Allting anropas med: _productConfigMapper.Map(WebFoundationProduct, pageId)
Konfigurerbar produkt modell Property mapper • Ett exempel på hur man gör en property mapper som returnerar true eller false om produkten finns i lager. (Notera att kravet att det skall vara en lista av strängar troligen kommer att ändras.) • public class IsInStockMapper : INamedPropertyMapper • { • private readonly IWarehouseService _warehouseService; • private readonly IsInStockMapperSettings _settings; • public IsInStockMapper(IWarehouseService warehouseService,IsInStockMapperSettings settings ) • { • _warehouseService = warehouseService; • _settings = settings; • } • public string PropertyName • { • get { return "Stock"; } • } • public List Map(WebFoundationProduct product,string pageId) • { • //TODO borde in i WarehouseServicen • double stockQuantity = 0; • if (product.IsVariantOwner) • { • stockQuantity = product.GetVariantMembers ().Sum(p=>_warehouseService.GetStockQuantity(p)); • } • else • { • stockQuantity = _warehouseService.GetStockQuantity(product); • } • return new List { (stockQuantity > _settings.InStockTreshold).ToString(CultureInfo.InvariantCulture) }; • } • public string DefaultVaryBy • { • get { return null; } • }
Konfigurerbar produkt modell xml konfiguration • För tillfället stöds bara xml konfiguration men backoffice stöd är önskvärt också. • I runtimme_settings.xml ställer man in följande exempel: • Location anger man i koden t.ex. produktsida, produktlistning, relaterade produkter dvs plats i siten. PageIdentifier är valfrit, är man på en sida som inte är konfigurerad går den på inställningar för t.ex. Location.Properties • Man kan ställa in att i produktlistningen skall t.e.x. Name läsas av även på dess varianter med: • För tillfället finns följande alternativ som man kan utöka genom arv eller interface (LookupVaryByProductService ): public HashSet GetVaryByProductList(WebFoundationProduct product, string varyBy) { switch (varyBy) { case "Product": return new HashSet () { product }; case "ProductOrVariants": if (product.IsVariantOwner) return new HashSet (m_productService.GetVariants(product)); return new HashSet () { product }; case "VariantDefault": return new HashSet () { m_variantService.GetDefaultVariant(product) ?? product }; case "Variants": return new HashSet ( m_productService.GetVariants(product) ); case "ProductAndVariants": var productList= new HashSet (m_productService.GetVariants(product)); productList.Add(product); return productList; default: return null; }
Konfigurerbar produkt modell Lathund • Fråga: Jag vill läsa av en property som det inte finns stöd för idag. Svar: Skapa en property mapper genom att implementera IPropertyMapper eller INamedPropertyMapper • Fråga: jag vill läsa av samma properties på alla varianter eller produkter som ingår i ett paket. Svar: Ärv/implementera (I)LookupVaryByProductService Eller skapa en ny property mapper som slår upp de andra produkterna och läser propertyn på dem också. • Fråga: Jag vill special anpassa cachningen själv. Svar: Implementera IPropertyValuesCacheService eller ärv/implementera (I)ProductConfigMapper. • Fråga: Jag vill inte bara returnera en dictionary med strängar utan en mer komplicerad objekt hiarki som produktens attribut kategoriserad på attributtyp. Svar: Skapa en specifik property mapper för det som inte returnerar en sträng. • Fråga: Jag har redan en typad modell och vill inte behöva skriva om allt. Svar: Det går att kombinera med befintliga kundlösningar genom att du på din befintliga model lägger till en property med typen ProductConfigViewModel och anropar ProductConfigMapper.Map. • Fråga: jag vill läsa av vissa properties på produkten när jag är i produktlistningen och vissa på produkt sidan. Svar: Man kan namnge olika sammanhang kallat location och t.om konfigurera per sida. • Fråga: När är det inte lämpligt att använda detta? Svar: För tillfället stöds inte formulär data med detta då man måste göra en ny modelbinder som förstår den otypade datan. Eller när affärslogik måste läsa av modellen. • Fråga: Kan inte prestandan bli dålig om jag måste slå upp samma Enova objekt flera gånger. Svar: Mellanlagra i httpcontext som t.e.x: public class AttributeValuesMapper : IPropertyMapper { protected EnovaAttributeType GetAttributeType(Context context, string property) { var key = "AttributeValuesAsStringMapper." + property; if (HttpContextFactory.Current.Items.Contains(key)) return HttpContextFactory.Current.Items[key] as EnovaAttributeType; var type = context.FindObject (property); HttpContextFactory.Current.Items[key] = type; return type; }
Mapper (Factory) • Mapper är ett försök till att förenkla Factory lagret i Webfoundation. • Motsvarar ungefär Factory.Create. • Factory namnet kan lätt förväxlas med design patternet medans Mapper begreppet är inarbetat som t.e.x. AutoMapper. Namnet förklarar mer vad det gäller. Skapa och mappa data från Enova till en vy model. • Modell klassen innehåller ingen kod bara properties. Pg.a. detta behöver modell klassen inget interface (Förutom om properties med validations attribut behöver variera med kundlösningar). • Model klassens properties bör alltid vara för formaterade strängar som url, förformaterade priser etc. Detta för att få bort logik i vyn och effektivisera cachning. • Mapper är inte singelton vilket gör att man enklare kan mellan lagra data som är jobbig att hämta i klassen. T.ex.. CartMapper vill kanske spara undan aktuell valuta etc istället för att slå upp det för varje cart rad. • Mappers har bara en Metod: viewModel Mapper.Map(EnovaObject). • Man skapar en mapper per model klass för singel responsible. Så CartMapper kan anropa CartItemMapper. • Stödjer Autofac på ett bättre sätt. • Mappers för anropa services mm. Man bör sträva efter att ha affärslogik i service klasserna och bara presentations logik i mappern. • Man bör försöka hålla sig till max två nivåer av mappers som anropar varrandra. Ett sätt är att platta ut model vyn och inte spegla enova objekt hiarkin rakt av. Eller anropa renderaction om modellen under inte är kopplad till huvud modellen.
Mapper Anrop • Mappers anropar man i sin controller genom att först anropa en service för att hämta grunden till sin enova data och sedan anropar man mapperns Map metod. public class CartController : Controller { private readonly ICartService _cartService; private readonly ICartMapper _mapper; public CartSurfaceController(ICartService cartService, ICartMapper mapper) { _cartService = cartService; _mapper = mapper; } public PartialViewResult MiniCart() { var cart = _cartService.GetCartOrCreateNew(); var model = _mapper.Map(cart); return PartialView(model); }
Mapper CartMapper model public class CartViewModel { public List CartItems { get; set; } public string TotalPriceWithCurrency { get; set; } } public class CartItemViewModel : ICartItemViewModel { public int ProductID { get; set; } public string ProductArtNr { get; set; } public string Name { get; set; } public string Description { get; set; } public string PriceWithCurrency { get; set; } }
Mapper CartMapper public interface ICartMapper { CartViewModel Map(EnovaCart cart); } public class CartMapper : ICartMapper { private readonly Func _createCartModel; private readonly ICartItemMapper _cartItemMapper; public CartMapper(Func createCartModel, ICartItemMapper cartItemMapper) { _createCartModel = createCartModel; _cartItemMapper = cartItemMapper; } public virtual ICartViewModel Map(EnovaCart cart) { var model = _createCartModel(); var context = cart.GetContext(); model.CartItems = cart.GetCartItems ().Select(item => _cartItemMapper.Map(item)).ToList(); model.TotalPriceWithCurrency = context.AmountToString(totalPrice, context.CurrentCurrency, context.CurrentCurrency.Decimals, true, true); return model; }
Mapper CartItemMapper public interface ICartItemMapper { CartItemViewModel Map(CartItem cart); } public class CartItemMapper : ICartItemMapper { private readonly Func _createCartItemModel; private readonly ImageService _imageService; protected bool _showPriceIncludingTax; public CartItemMapper(Func createCartItemModel, ImageService imageService) { _createCartItemModel = createCartItemModel; _imageService = imageService; _showPriceIncludingTax = true; } public virtual ICartItemViewModel Map(CartItem cartItem) { var context = cartItem.GetContext(); // General var model = _createCartItemModel(); model.Name = cartItem.Name; // Product if (cartItem is EnovaProductCartItem) { var productCartItem = cartItem as EnovaProductCartItem; model.ProductArtNr = productCartItem.ProductIdentifier; model.Quantity = productCartItem.Quantity; model.Image = _imageService.GetImage(productCartItem.Product, ImageService.ImageSize.Small); model.PriceWithCurrency = context.AmountToString(productCartItem.Product.Price, context.CurrentCurrency, context.CurrentCurrency.Decimals, true, true); } return model; } }
Mapper (Factory) • Möjligen kanske alla mappers bör vara konfigurerbara mappers. • I exemplet innan med cart och cartitems kan man tänka sig en property mapper som klarar av cartitems och som för varje item anropar ConfigMapper.
MVC kontrollbaserat • I Webfoundation kör man strikt MVC, dvs en model som typat är komplett för en hel sida, vilket har en del nackdelar.
MVC kontrollbaserat • Kör man kontroll baserat dvs där man i vyn anropar RenderAction får man en mycket enklare lösning och mer lös kopplat. • Färre nivåer i modellen och factories som anropar varrandra. • Lättare att lägga till och ta bort saker på sidan. Som t.ex. lägga till en modell för twitter på sidan eller ta bort model för varianters färger. • Alla vyerna och koden för en kontroll kan man enklare samla ihop i ett projekt eller en folder. Vilket enklare gör att man kan återanvända eller anpassa koden. • Enklare att output cacha delar av sidan.
MVC kontrollbaserat • Det betyder inte att man struntar i att separera ui från logik som i web.forms. • I exemplet med produktlistning, kan man t.e.x. lägga följande klasser i samma folder: – ProductListController med en subaction – Modell klasserna. – Eventuella servicar specifikt för denna. – Vyerna måste tyvärr läggas separat under views men kan samlas under foldern ProductList. • På så sätt ser man enklare var gemensamma funktioner ligger och man bör sträva efter att försöka kategorisera ner de under respektive kontroll folder såvida det inte är en generell funktion. Detta kan leda till att det blir enklare att stycka upp stora servicar. • Man bör dock kanske inte gå till absurdum som i web.forms kontroller som pratar med varandra och undvika renderaction i stora for loopar.
MVC kontrollbaserat Exempel sida • Ett exempel på en sida och vilka kontroller den skulle kunna delas in i (hela sidan anropas av en huvud controller med sidans cms data): Nyheter Produktlistning Topmeny Sök Varukorg Reklambanner Produkt tips Login (Betyg)
MVC kontrollbaserat Exempel kod • Exempel på hur det kan se ut i vyn: – • I Controllern (notera hur jag kan få aktuell sida som inparameter då all routing data från sidan automatiskt skickas vidare.) [ChildActionOnly] public PartialViewResult List(int cmsid, string layout) { var pageId = cmsid.ToString();
Autoregistrering med Autofac • Målet är att utvecklaren inte skall behöva bry sig så mycket om Autofac och dess komplexitet. Utvecklaren skapar bara en klass vars namn slutar med t.ex. Service så kan han injekta servicen sen med dess interface. Vill man byta ut en klass i Webfoundation skapar utvecklaren bara en klass som implementerar samma interface elelr ärver av webfoundation klasssen (Allt utan att behöva skriva någon Autofac registering). • Förenklare modularisering av sin kod då man inte har registrering i en jätte modul eller applikations start. • Auto registrerar alla typer i ens projekt genom namn konventioner eller klass attribut. • Ta bort servicelocator klasser från WF och istället köra med strikt konstruktor injection. (Inget med autoregistrering men borde göras)
Autoregistrering med Autofac Exempel kod • Registrera din Autofac modul på standard sätt, fast istället för att fylla den med massa registreringar anropa RegisterAllKnownServicesInAssembly, te.x. public class MyModule : Module { protected override void Load(ContainerBuilder builder) { //will autoregister all types in this assembly with naming convention builder.RegisterAllKnownServicesInAssembly(this.GetType().Assembly); • Som valfria argument kan man skicka in vilka typer som inte skall registreras och vilka namn konventioner som skall gälla. Detta kan man även ställa in i web.config
Autoregistrering med Autofac web.config • Följande web.config inställningar finns: • WebFoundation.AutoRegister.ExceptTypes Gör att ingen autoregistrering kommer att ske på önskade typer (komma separerat fullständigt namn) som standard blank. • WebFoundation.AutoRegister.singletonNameConventionEndsWith Autogeristrera alla klasser som singelton vars klass namn slutar med som standard: "Service", "Repository", "Provider", "Listener","Factory" • WebFoundation.AutoRegister.newInstanceEachTimeNameConventionEndsWith Autogeristrera alla klasser att skapas upp alltid vars klass namn slutar med som standard: "Model", "Mapper", "Filter", "ViewData", "Settings" • WebFoundation.AutoRegister.httpRequestNameConventionEndsWith Autogeristrera alla klasser att skapas upp per http request vars klass namn slutar med som standard: För tillfället inga då det är lite besvärligt att http request måste vara tillgängligt och att den inte nergraderar till ny instans varje gång annars. • WebFoundation.AutoRegister.containerNameConventionEndsWith Autogeristrera alla klasser att skapas upp per scope vars klass namn slutar med som standard: inga. • Standard värdena kan ändras. Fördelen med namn konventioner är att man mer kommer att följa dem och att man får rätt livstid på objekten utan att behöva tänka på det.
Autoregistrering med Autofac Attribut • Om man inte vill använda sig av namn konvention kan man använda sig av följande klass attribut. • ExcludeFromAutoregister Klassen följer en namn konvention men man vill inte registrera den automatiskt. • Alla följande attribut har ett name argument om man vill registrera klassen med ett namn som t.ex. en MVC controller som kan se ut så här: [AutoRegister(Named = "controller.newspage")] public class NewsPageController : FoundationController { • AutoRegisterAsSingelton • AutoRegisterAsNewInstanceEachTime • AutoRegisterAttribute Samma som AutoRegisterAsNewInstanceEachTime • AutoRegisterAsContainer • AutoRegisterAsHttpRequest