Inhalte aufrufen

Profilbild
- - - - -

[Feature request] Controller Erweiterbarkeit


Best Answer Mark Wördenweber , 17 April 2023 - 13:41

Hey,

 

Änderungen am Core sollten nur vorgenommen werden, wenn es nicht anders geht.
Die Implementierung über ein Modul ist in diesem Fall die bessere Lösung und macht den Code flexibler.
Ich schicke dir per PM noch ein paar hilfreiche Links dazu.

Dieses Problem lässt sich mit ActionFilter lösen. Dazu wird ein ActionFilter z.B. ProductDetailsFilter.cs verwendet.
 

public class ProductDetailsFilter : IAsyncActionFilter
{
	public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
	{
		// Vor der Ausführung.

		var executedContext = await next();

		// Nach der Ausführung.

		if (executedContext.Result is not ViewResult result)
		{
			return;
		}

		if (result.Model is not ProductDetailsModel model)
		{
			return;
		}

		await ChangeModelAsync(model);
	}
	public async Task ChangeModelAsync(ProductDetailsModel model)
	{
		// Änderungen am Model.
	}
}

Zusätzlich muss der Filter in der StartUp.cs registriert werden.

internal class Startup : StarterBase
{
	public override void ConfigureServices(IServiceCollection services, IApplicationContext appContext)
	{
		services.Configure<MvcOptions>(o =>
		{
			o.Filters.AddConditional<ProductDetailsFilter>(
				context => context.ControllerIs<ProductController>(x => x.ProductDetails(0, null)));

		});
	}
}

Beim Checkout ist das Prinzip ähnlich. Nur wird hier der CheckoutController für die Registrierung verwendet.

Go to the full post


  • Bitte melden Sie sich an, um eine Antwort zu verfassen.
5 Antworten zu diesem Thema

#1 Algorithman

Algorithman

    Advanced Member

  • Members
  • PunktPunktPunkt
  • 39 Beiträge

Geschrieben: 17 April 2023 - 08:24

Guten Morgen,

 

 

Wir sind ein Brillenhersteller und möchten gerne auf SmartStore umsteigen.
Da wir aber als B2B-Shop zusätzliche Anforderungen haben (zusätzliche Daten zu jedem Produkt als default Werte) bin ich jetzt gerade dabei, diese zu integrieren.
Um diese Daten aber in den jeweiligen View (meist ProductsController, aber auch im Checkout etc müssen die vom Kunden geänderten Daten vorhanden sein) zu integrieren, fehlen mir noch ein paar Hook-Möglichkeiten.

 

Um dabei Merge-Conflicts weitestgehend aus dem Weg zu gehen, könnte man die Controller nicht in folgender Weise erweitern:

(Beispiel am ProductsController)

 

        public async Task<IActionResult> ProductDetails(int productId, ProductVariantQuery query)        {
            // Sync on purpose because of large column.
            var product = await _db.Products
                .AsSplitQuery()
                .IncludeMedia()
                .IncludeManufacturers()
                .FindByIdAsync(productId);


            if (product == null || product.IsSystemProduct)
                return NotFound();


            // Is published? Check whether the current user has a "Manage catalog" permission.
            // It allows him to preview a product before publishing.
            if (!product.Published && !await Services.Permissions.AuthorizeAsync(Permissions.Catalog.Product.Read))
                return NotFound();


            // ACL (access control list).
            if (!await _aclService.AuthorizeAsync(product))
                return NotFound();


            // Store mapping.
            if (!await _storeMappingService.AuthorizeAsync(product))
                return NotFound();


            // Is product individually visible?
            if (product.Visibility == ProductVisibility.Hidden)
            {
                // Find parent grouped product.
                var parentGroupedProduct = await _db.Products.FindByIdAsync(product.ParentGroupedProductId, false);
                if (parentGroupedProduct == null)
                    return NotFound();


                var seName = await parentGroupedProduct.GetActiveSlugAsync();
                if (seName.IsEmpty())
                    return NotFound();


                var routeValues = new RouteValueDictionary
                {
                    { "SeName", seName }
                };


                // Add query string parameters.
                Request.Query.Each(x => routeValues.Add(x.Key, Request.Query[x.Value].ToString()));


                return RedirectToRoute("Product", routeValues);
            }


            // Prepare the view model
            var model = await _helper.MapProductDetailsPageModelAsync(product, query);


            // Some cargo data
            model.PictureSize = _mediaSettings.ProductDetailsPictureSize;
            model.HotlineTelephoneNumber = _contactDataSettings.HotlineTelephoneNumber.NullEmpty();
            if (_seoSettings.CanonicalUrlsEnabled)
            {
                model.CanonicalUrl = Url.RouteUrl("Product", new { model.SeName }, Request.Scheme);
            }


            model.MetaProperties = await model.MapMetaPropertiesAsync();


            // Save as recently viewed
            _recentlyViewedProductsService.AddProductToRecentlyViewedList(product.Id);


            // Activity log
            Services.ActivityLogger.LogActivity(KnownActivityLogTypes.PublicStoreViewProduct, T("ActivityLog.PublicStore.ViewProduct"), product.Name);


            // Breadcrumb
            if (_catalogSettings.CategoryBreadcrumbEnabled)
            {
                await _helper.GetBreadcrumbAsync(_breadcrumb, ControllerContext, product);


                // 'Continue shopping' URL.
                var customer = Services.WorkContext.CurrentCustomer;
                if (!customer.IsSystemAccount)
                {
                    var categoryUrl = _breadcrumb.Trail?.LastOrDefault()?.GenerateUrl(Url);
                    if (categoryUrl.HasValue())
                    {
                        customer.GenericAttributes.LastContinueShoppingPage = categoryUrl;
                    }
                }


                _breadcrumb.Track(new MenuItem
                {
                    Text = model.Name,
                    Rtl = model.Name.CurrentLanguage.Rtl,
                    EntityId = product.Id,
                    Url = Url.RouteUrl("Product", new { model.SeName })
                });
            }
//////////////////////////////////////////////////////////////////////////////////
// Anstatt direkt das Model zurückzugeben zuerst die virtuelle Methode aufrufen.
//////////////////////////////////////////////////////////////////////////////////

            var optionallyChangedModel = await ChangeModel(model);

            return View(optionallyChangedModel.ProductTemplateViewPath, optionallyChangedModel);
        }



 // Virtuelle Methode, um in abgeleiteten Controllern erweiterte Models zurückzuliefern 
 // ohne die Logik des Shops zu berühren
        public virtual async Task<ProductDetailsModel> ChangeModel(ProductDetailsModel model)
        {
            return model;
        }

Minimale Änderung am Controller selbst, aber damit könnte man dann im eigenen Modul den Controller ableiten und die zusätzliche benötigten Daten nachladen und das erweiterte ProductDetailsModel zurückliefern,

 public class AdjustedProductController : ProductController
    {
        private DbSet<AdditionalProductData> _additionalProducts;

        protected DbSet<AdditionalProductData> AdditionalProducts
        {
            get => _additionalProducts ??= _db.Set<AdditionalProductData>();
            set => _additionalProducts = value;
        }

        private readonly SmartDbContext _db;

        public AdjustedProductController(SmartDbContext db, IWebHelper webHelper, IProductService productService,
            IProductAttributeService productAttributeService, IRecentlyViewedProductsService recentlyViewedProductsService, IAclService aclService,
            IStoreMappingService storeMappingService, IMediaService mediaService, ICustomerService customerService, MediaSettings mediaSettings,
            CatalogSettings catalogSettings, CatalogHelper helper, IBreadcrumb breadcrumb, SeoSettings seoSettings,
            ContactDataSettings contactDataSettings, CaptchaSettings captchaSettings, LocalizationSettings localizationSettings,
            PrivacySettings privacySettings, Lazy<IMessageFactory> messageFactory, Lazy<ProductUrlHelper> productUrlHelper,
            Lazy<IProductAttributeFormatter> productAttributeFormatter, Lazy<IProductAttributeMaterializer> productAttributeMaterializer,
            Lazy<IStockSubscriptionService> stockSubscriptionService) : base(db, webHelper, productService, productAttributeService,
                recentlyViewedProductsService, aclService, storeMappingService, mediaService, customerService, mediaSettings, catalogSettings, helper,
                breadcrumb, seoSettings, contactDataSettings, captchaSettings, localizationSettings, privacySettings, messageFactory,
                productUrlHelper, productAttributeFormatter, productAttributeMaterializer, stockSubscriptionService)
        {
            _db = db;
        }


       public override async Task<ProductDetailsModel> ChangeModel(ProductDetailsModel model)        {
            var additionalProductDetailsModel = await MapperFactory.MapAsync<ProductDetailsModel, AdjustedProductDetailsModel>(model);
            var addData = await AdditionalProducts.FirstOrDefaultAsync(x => x.ProductId == additionalProductDetailsModel.Id);
            if (addData != null)
            {
                additionalProductDetailsModel.AdditionalData = await MapperFactory.MapAsync<AdditionalProductData, AdditionalProductDataModel>(addData);
            }


            return additionalProductDetailsModel;
        }
    }

um dann im angepassten View Zugriff auf die zusätzlichen Daten zu haben.

Hab das ganze auch schon ausprobiert, funktionieren würde es tadellos.

 

AdditionalProductDataModel hat auch nur ein paar string-Properties, die eben aus einer zusätzlichen Table ausgelesen werden sollen als Default-Werte pro Produkt, aber vom Kunden änderbar sein sollen. 

 

Das würde die Erweiterbarkeit des Shops sehr vereinfachen und Merge-Conflicts wären praktisch ausgeschlossen.

 

Namen etc der Methoden/Variablen sind natürlich subject to change. Hab das nur mal als proof of concept gemacht, und ich glaube, dass das eine sinnvolle Erweiterung wäre für den Shop an sich.
Viele Kunden haben spezielle Anforderungen, die sich so relativ einfach lösen lassen würden.

 

Wäre schön, wenn wir uns über das Thema mal austauschen könnten. 

 

Ich könnte auch die Änderungen mal als PR machen, macht allerdings nur Sinn, wenn die Benamungsstrategie klar wäre und das überhaupt gewollt wird.

 

 

MfG

 

 

Chris


  • GalenKa gefällt das

#2 Mark Wördenweber

Mark Wördenweber

    Smartstore Moderator

  • Administrators
  • 5 Beiträge

Geschrieben: 17 April 2023 - 13:41   Best Answer

Hey,

 

Änderungen am Core sollten nur vorgenommen werden, wenn es nicht anders geht.
Die Implementierung über ein Modul ist in diesem Fall die bessere Lösung und macht den Code flexibler.
Ich schicke dir per PM noch ein paar hilfreiche Links dazu.

Dieses Problem lässt sich mit ActionFilter lösen. Dazu wird ein ActionFilter z.B. ProductDetailsFilter.cs verwendet.
 

public class ProductDetailsFilter : IAsyncActionFilter
{
	public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
	{
		// Vor der Ausführung.

		var executedContext = await next();

		// Nach der Ausführung.

		if (executedContext.Result is not ViewResult result)
		{
			return;
		}

		if (result.Model is not ProductDetailsModel model)
		{
			return;
		}

		await ChangeModelAsync(model);
	}
	public async Task ChangeModelAsync(ProductDetailsModel model)
	{
		// Änderungen am Model.
	}
}

Zusätzlich muss der Filter in der StartUp.cs registriert werden.

internal class Startup : StarterBase
{
	public override void ConfigureServices(IServiceCollection services, IApplicationContext appContext)
	{
		services.Configure<MvcOptions>(o =>
		{
			o.Filters.AddConditional<ProductDetailsFilter>(
				context => context.ControllerIs<ProductController>(x => x.ProductDetails(0, null)));

		});
	}
}

Beim Checkout ist das Prinzip ähnlich. Nur wird hier der CheckoutController für die Registrierung verwendet.


  • Algorithman gefällt das

#3 Algorithman

Algorithman

    Advanced Member

  • Members
  • PunktPunktPunkt
  • 39 Beiträge

Geschrieben: 17 April 2023 - 14:19

Vielen Dank

 

Im Prinzip würde das gehen, wenn ich keine zusätzlichen Felder in der Products Klasse haben wollte bzw. nur Änderungen an bestehenden Feldern vorgenommen werden müssten. Da ich aber zusätzliche Felder benötige hilft mit das jetzt leider nicht weiter, da im result das Model leider read-only ist. (ausser ich geh da vielleicht mit Reflection ran.....)

 

Ich würde eben gerne in meinen Views für die Produkte zusätzliche Felder anzeigen.

public class ExtendedProduct : Product
{
    public string Feld1 {get;set;}
    public string Feld2 {get;set;}
    public string Feld3 {get;set;}
    public string Feld4 {get;set;}
    public string Feld5 {get;set;}
etcpp.
}

Oder gibt es vielleicht die Möglichkeit die Product-Klasse auf meine 'umzulegen' und das ganze dann über DB-Hooks zu erledigen?

 

MfG

 

Chris



#4 Algorithman

Algorithman

    Advanced Member

  • Members
  • PunktPunktPunkt
  • 39 Beiträge

Geschrieben: 17 April 2023 - 14:29

Oh, ich hab ne Möglichkeit gefunden:
 

executedContext.Result = (executedContext.Controller as ProductController).View(result.ViewName, await ChangeModelAsync(model));

Ob das jetzt aber performancetechnisch so gut ist, weiß ich nicht :)



#5 Mark Wördenweber

Mark Wördenweber

    Smartstore Moderator

  • Administrators
  • 5 Beiträge

Geschrieben: 18 April 2023 - 08:24

Es ist keine gute Idee, im Core herumzubasteln.
Am besten erstellst du für dein ExtendedProduct ein Domain-Objekt mit einer ProductId als Produktreferenz und den benötigten Feldern.

Schau dir dazu das Domain-Tutorial an.


  • stefanmueller gefällt das

#6 Algorithman

Algorithman

    Advanced Member

  • Members
  • PunktPunktPunkt
  • 39 Beiträge

Geschrieben: 18 April 2023 - 08:49

Natürlich ist das keine gute Idee, deswegen hab ich ja geschrieben, dass ich merge-conflicts weitestgehend vermeiden will. Mir war nur nicht klar, dass ich das über die ActionFilter erledigen kann.

Und meine Daten sind schon in einer eigenen Table, das Domain-Tutorial hab ich schon durch :)

 

Dieser Thread kann als gelöst markiert werden.


  • stefanmueller gefällt das