Inhalte aufrufen

Profilbild
- - - - -

Anpassung von Views


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

#1 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 26 May 2014 - 16:23

Ein großes Ziel bei der Erweiterung des Shops ist der Erhalt der Releasefähigkeit. Leider haben die Anpassungsmöglichkeiten im Rahmen eines Plugins ihre Grenzen, wenn es darum geht, bestehende Views um Informationen zu erweitern. Ein Beispiel hierfür ist die Preisdarstellung, da der Shop immer von der Verkaufsgröße 1 ausgeht, es bei unseren Kunden aber durchaus möglich ist, dass der Preis pro 100 Stk gilt. Zudem fehlt die Angabe der Verkaufseinheit in der Preisdarstellung, was für uns insbesondere wichtig ist, wenn es sich eben nicht um Stück handelt oder der Artikel mit unterschiedlichen Verkaufseinheiten (Stück, Pack, Karton, Palette) angeboten wird.

 

Wir sind jetzt folgendermaßen vorgegangen:

 

Im Namespace SmartStore.Models haben wir eine neue Datei _NVExtModel eingefügt, in welches wir die Model-Extensions als partial class schreiben. Damit bleiben die SmartStore-Modele unangetastet, können aber trotzdem beliebig erweitert werden.

Da die Modelle in den jeweiligen Controller-Actions gefüllt werden, wollten wir einen Mechanismus schaffen, der uns die Model befüllen lässt, ohne dass wir den Code hierfür direkt in den Controller schreiben. Da sich die Controller in einem Plugin nicht ableiten lassen, um gezielt Methoden zu überschreiben, haben wir uns dafür entschieden, auch hier den Weg über die Partial Class zu gehen. Wir haben also in den Namespace SmartStore.Web.Controllers eine Datei _NVExtController eingefügt, welche Partial Classes für die Controller enthält. Soll ein Model angepasst werden (in unserem Test z.B. das ProductOverviewModel), legen wir hier eine entsprechende Methode „Customize“ an.

namespace SmartStore.Web.Controllers
{
    public partial class CatalogController
    {
        protected IEnumerable<ProductOverviewModel> Customize(IEnumerable<ProductOverviewModel> products)
        {
            return CustomizingManager.Customize(products);
        }
    }
}

In dem Controller ist die einzige Anpassung der Aufruf der Customize-Methode

protected IEnumerable<ProductOverviewModel> PrepareProductOverviewModels(
         //…
         return Customize(models);
}

Der Customizing-Manager arbeitet ähnlich wie der RouteProvider. Beim Starten der Applikation wird geprüft, ob ein Plugin über eine Implementierung des ICustomizingProvider-Interface verfügt, und ggf. die Interface-Methode RegisterCustomizings aufgerufen.

namespace SmartStore.Web.Framework.Customizer
{
    public class CustomizingPublisher : ICustomizingPublisher
    {
        private readonly ITypeFinder _typeFinder;
 
        public CustomizingPublisher(ITypeFinder typeFinder)
        {
            this._typeFinder = typeFinder;
        }
    
        public void RegisterCustomizings()
        {
                  var customizingProviderTypes = _typeFinder.FindClassesOfType<ICustomizingProvider>();
            var customizingProviders = new List<ICustomizingProvider>();
 
            foreach (var providerType in customizingProviderTypes)
            {
                if (!PluginManager.IsActivePluginAssembly(providerType.Assembly))
                {
                    continue;
                }
                
                var provider = Activator.CreateInstance(providerType) as ICustomizingProvider;
                customizingProviders.Add(provider);
            }
 
            customizingProviders.Each(cp => cp.RegisterCustomizings());
        }
    }
}

********************

namespace SmartStore.Plugin.Import.eNVenta
{
    public class CustomizingProvider :ICustomizingProvider
    {
        public void RegisterCustomizings()
        {
            CustomizingManager.Register<IEnumerable<ProductOverviewModel>>(CustomModel);
            CustomizingManager.Register<PictureModel>(CustomModel2);
        }
 
        public IEnumerable<ProductOverviewModel> CustomModel(IEnumerable<ProductOverviewModel> products)
        {
            foreach (var product in products)
            {
                product.NVArticleID = "TEST";
            }
 
            return products;
        }
 
        public PictureModel CustomModel2(PictureModel products)
        {
            return products;
        }
    }
} 

********************

 

Das Interface erlaubt das Registrieren von Delegates, welche mit dem anzupassenden Typen übereinstimmen. In dem Test wurde das Customizing für List<ProductOverviewModel> durchgeführt. Durch diesen Mechanismus ist es jetzt möglich, die Logik für das ermitteln der individuellen Informationen in das Plugin auszulagern, wo z.B. auch der externe Webservice oder die Custom-Repositories zur Verfügung stehen.

 

Der CustomizingManager hat jetzt noch die Aufgabe, die entsprechenden Delegates, sofern verfügbar, aufzurufen

public class CustomizingManager
{
     private static Dictionary<Type, object> delegateMap = new Dictionary<Type, object>();
 
     public static T Customize<T>(T t)
     {
         var result = t;
 
         object tmp;
         if (delegateMap.TryGetValue(typeof(T), out tmp))
         {
             List<Func<T, T>> list = (List<Func<T, T>>) tmp;
             foreach (var action in list)
             {
                 result = action(result);
             }
         }
 
         return result;
     }
 
     public static void Register<T>(Func<T, T> method)
     {
         object tmp;
         if (!delegateMap.TryGetValue(typeof(T), out tmp))
         {
             tmp = new List<Func<T, T>>();
             delegateMap[typeof(T)] = tmp;
         }
            
         List<Func<T, T>> list = (List<Func<T, T>>) tmp;
         list.Add(method);
     }
 }

Wir können durch diesen Ansatz die Logik zur Ermittlung der Inhalte in das Plugin auslagern, wo uns der WebService bzw. unser ObjectContext zur Verfügung steht. Für die Darstellung wird uns nichts anderes übrig bleiben, als die Views direkt anzupassen. Trotzdem finde ich die Lösung recht Elegant, weil der Eingriff in die Controller überschaubar bleibt.



#2 Wolfgang Schmerge

Wolfgang Schmerge

    SmartStore AG

  • Administrators
  • 2449 Beiträge

Geschrieben: 26 May 2014 - 16:49

Hallo,

 

vielen Dank für die interessanten Informationen.

Kann man sich die Änderungen schon irgendwo in einem Shop anschauen?

 

Liebe Grüße

 

Wolfgang


Shopbetreiber benötigen Ihre Hilfe! Bewerten Sie jetzt Smartstore auf Capterra.

Als Dankeschön erhalten Sie 20 Euro für Ihren nächsten Kauf im Marketplace.

Smartstore bewerten


 

Bleibt gesund!

 

Viele zusätzliche Smartstore Plugins gibt es im MARKETPLACE:
http://community.sma...dex.php?/files/

 

Hier geht es zu den Smartstore Videos:
Smartstore.NET Youtube-Channel
 

Die deutsche Smartstore Online-Dokumentation gibt es hier:
https://smartstore.a...iew?mode=global

In dem folgenden BLOG findet man interessante Tipps & Tricks zum Thema "Smartstore":

http://community.sma...t-tipps-tricks/

 


#3 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 26 May 2014 - 17:46

Hallo Wolfgang,

 

ja, wir haben das SmartStore-Projekt auf Codeplex nach SmartStoreNV geforked, Branch: feature-nverp. Hier sind die Erweiterungen von heute eingecheckt.

 

Eine Demo ist im Internet aber noch nicht verfügbar, da wir noch sehr viele Tasks offen haben. Bis jetzt geht es uns hauptsächlich um die Möglichkeiten eines nachhaltigen Change-Management. Was sonst passieren kann, kennen wir durch ERP-Software zu genüge :D. Deshalb ist uns auch die Code-Separation sehr wichtig.

 

Viele Grüße

 

Holger



#4 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 26 May 2014 - 17:46

 

 

Leider haben die Anpassungsmöglichkeiten im Rahmen eines Plugins ihre Grenzen, wenn es darum geht, bestehende Views um Informationen zu erweitern. Ein Beispiel hierfür ist die Preisdarstellung, da der Shop immer von der Verkaufsgröße 1 ausgeht, es bei unseren Kunden aber durchaus möglich ist, dass der Preis pro 100 Stk gilt. Zudem fehlt die Angabe der Verkaufseinheit in der Preisdarstellung, was für uns insbesondere wichtig ist, wenn es sich eben nicht um Stück handelt oder der Artikel mit unterschiedlichen Verkaufseinheiten (Stück, Pack, Karton, Palette) angeboten wird

 

Ein entsprechender Issue zur Implementierung von QuantityUnits und PriceUnits existiert bereits. Wir hoffen diese bereits in V2.1 implementieren zu können, können das aber nicht garantieren (spät. jedoch zu V2.5).

 

Zu deiner Lösung: unter den gegebenen Umständen halte ich deinen Hooking-Ansatz für optimal! Wie du richtig bemerkt hast sind im Moment bei Plugins klare Grenzen gezogen, wenn es um funktionale Erweiterung des Kerns geht. Auch das werden wir in Zukunft ändern, was aber einen enormen Refactoring-Aufwand mit sich bringt. Daher haben wir noch keine Timeline hierfür. Aber bis dahin fährst du m.E. mit deiner Lösung sehr gut.


Murat Cakir
SmartStore AG


#5 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 26 May 2014 - 17:54

Noch ein Tipp: du könntest vielleicht mehr Kapselung erreichen, wenn du deine Model-Extensions in die CustomProperties-Dictionary der ModelBase-Basisklasse packst (welche von allen Model-Klassen in SmartStore.Web geerbt wird). Auf diese Weise könntest du die Model-Erweiterungen - in welcher Form auch immer - in deinem Plugin-Projekt belassen. Außerdem bräuchte dein Plugin SmartStore.Web nicht mehr zu referenzieren.

 

Edit: Vergiss diesen Bullshit  :) Ich habe geschrieben, während ich gedacht habe... das geht nicht, weil du dann auch deine Views ins Plugin verlagern müsstest. Es gibt zwar die Möglichkeit, über sog. SimpleWidgets (IWidget) Partials von extern in Widget-Zonen zu injizieren, aber das würde den Code relativ unübersichtlich machen. Einfacher ist besser!


Murat Cakir
SmartStore AG


#6 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 27 May 2014 - 14:06

Vielen Dank für das Feedback,

 

über das Dictionary könnte man in der Tat auf die Erweiterung der Models über Partial Classes verzichten, müsste aber zusätzlichen Code einplanen, um die Eigenschaften für die Darstellung erst mal wieder in den richtigen Typ zu Casten. Da man wegen der Views nicht auf ein Anpassen des Standard verzichten kann, haben wir uns entschlossen, lieber die Klassen typsicher zu erweitern, was wie beschrieben ja sehr zentral lösbar ist. Vielleicht wäre an der Stelle ein dynamic-Container wie der ViewBag noch besser? Habe mit dem aber selber auch noch nicht wirklich gearbeitet. Die Views durch Custom-Views im Plugin zu ersetzen könnte in Zukunft eine sehr elegante Möglichkeit für die Anpassung bieten. Das birgt aber immer das Risiko, dass man sich durch die Nutzung der Kopie vom Standard abkoppelt und Korrekturen / Änderungen in der Zukunft nicht mitbekommt. Werden die Änderungen gemergt, passiert das nicht bzw. fällt das beim migrieren auf.

 

Was ich aber noch nicht probiert habe, ist ob man in die Standard-Views relativ leicht Partial-Views aus dem Plugin referenzieren kann. So könnte wie im aktuellen Ansatz darauf verzichten, die Logik im Standard zu erweitern und einfach nur die Einstiegspunkt nachziehen.



#7 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 27 May 2014 - 17:55

 

 

Was ich aber noch nicht probiert habe, ist ob man in die Standard-Views relativ leicht Partial-Views aus dem Plugin referenzieren kann. So könnte wie im aktuellen Ansatz darauf verzichten, die Logik im Standard zu erweitern und einfach nur die Einstiegspunkt nachziehen.

 

Das geht über's IWidget Interface. In einem Plugin (oder sonstwo) können beliebig viele sog. SimpleWidgets definiert werden. Beispiel:

public class MySimpleWidget : IWidget
{
    // Alle WidgetZone-Namen zurückgeben, in denen das Widget gerendert werden kann.
    // "productdetails_top" bspw. ist eine Zone in ~/Views/Catalog/ProductTemplate.Simple.cshtml
    public IList<string> GetWidgetZones()
    {
        return new List<string>() { "productdetails_top" };
    }
    
    // Definiert die aufzurufende Child-Action innerhalb deines Plugin-Controllers.
    // In diesem Beispiel "MyPluginController.MyWidget()"
    public void GetDisplayWidgetRoute(string widgetZone, out string actionName, out string controllerName, out RouteValueDictionary routeValues)
    {
        actionName = "MyWidget";
        controllerName = "MyPlugin";
        routeValues = new RouteValueDictionary()
        {
            {"Namespaces", "SmartStore.Plugin.MyPlugin.Controllers"},
            {"area", null},
            {"param1", something},
            {"param2", somethingElse}
        };
    }
}

Widgets müssen nicht registriert werden. Sie werden bei AppStart von Autofac gesammelt.


Murat Cakir
SmartStore AG


#8 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 28 May 2014 - 15:44

Ich habe mir das IWidget-Interface angesehen. Ich würde den Mechanismus jetzt so einordnen, dass man damit eigene PartialViews in den jeweiligen Widget-Zones anzeigen kann. Für mich stellt sich dann natürlich die Frage, wie ich eine Verbindung zum Model des gerenderten Views aufbaue, um z.B. erweiterte Informationen zu einem Produkt anzuzeigen. Ich kann die Parameter something und somethingElse noch nicht 100% einordnen, aber wenn ich den Code richtig gelesen habe, kann man hier nur feste Werte für die Widget-Controller Actions definieren.

 

 

 

Um auf das Model des aktuellen Controller zuzugreifen gibt es ja noch die Möglichkeit über den ControllerContext

public class NVWidgetController : SmartController
    {
        [ChildActionOnly]
        public ActionResult ProductViewEnrichment()
        {
            ProductDetailsModel productOverview = ControllerContext.ParentActionViewContext.ParentActionViewContext.ViewData.Model as ProductDetailsModel;
            return View("SmartStore.Plugin.Import.eNVenta.Views.NVWidget._ProductEnrichmentView");
        }
    }

Richtig gut ist der Ansatz nicht, weil mir nur das Site-Model zur Verfügung steht, in einer Liste aber das Model des jeweiligen Items als Context benötigt wird. ParentActionViewContext.ParentActionViewContext sollte eigentlich immer gleich bleiben, da der Parent meines Widget ja der WidgetController ist, dessen Parent wiederum der Controller der Site ist, in dem der Html-Helper eingebunden ist.

 

Ich habe noch keine konkrete Idee, wie sich das optimal lösen lässt, aber evtl. gibt es ja noch die Möglichkeit, das man dem Html-Helper das jeweilige Context-Model übergibt, und dieser das RouteValueDictionary an die gewünschten Parameter im RouteValue-Dictionary mappt...



#9 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 28 May 2014 - 16:29

Widgets sind leider nicht dafür konzipiert, mit dem Parent-ViewModel zu interagieren. Sie sind autark. Ich würde gar nicht erst versuchen, diesen Umstand zu umschiffen, indem man sich bspw. das ViewModel über ParentActionViewContext besorgt o.Ä. Man könnte sich da sicher etwas basteln, das funktioniert, aber nur mit Verzicht auf Coding-Komfort. Daher: besser es verbleibt alles in SmartStore.Web, am besten in einem neuen Theme. In diesem Theme überschreibt man nur die Views, die es anzupassen gilt. Alle neuen Partials wären ebenfalls Teil des Themes.

 

Ein bisschen Aufwand wird man vielleicht nach einem Programm-Update damit haben, evtl. View-Modifikationen zu übertragen... wird sich aber in Grenzen halten.


Murat Cakir
SmartStore AG


#10 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 30 May 2014 - 12:39

Hallo,

 

ich habe mir das Thema noch einmal ein bisschen durch den Kopf gehen lassen. Der Context in den Widgets wäre für uns schon ein must-Have, deshalb habe ich folgende Lösung erarbeitet:

 

Ziel: Verfügbarkeit des (Context)Models in meinen Widgets

 

1. Erweiterung von IWidget um einen Parameter "contextModel"

 /// <summary>
    /// Provides an interface for creating widgets
    /// </summary>
    public partial interface IWidget
    {
        /// ...
        void GetDisplayWidgetRoute(string widgetZone, object contextModel, out string actionName, out string controllerName, out RouteValueDictionary routeValues);
    }

Dann habe ich entsprechend den Aufruf in meiner Widget-Klasse angepasst:

public void GetDisplayWidgetRoute(string widgetZone, object contextModel, out string actionName, out string controllerName, out RouteValueDictionary routeValues)
        {
            actionName = "ManufacturerExt";
            controllerName = "NVWidget";
            routeValues = new RouteValueDictionary()
            {
                {"Namespaces", "SmartStore.Plugin.Import.eNVenta.Controllers"},
                {"area", null},
                {"widgetZone", widgetZone},
                {"model", contextModel}
            };
        }

der neue Parameter ist jetzt natürlich der Parameter für meine Widget-Controller Action:

public class NVWidgetController : SmartController
    {
        [ChildActionOnly]
        public ActionResult ManufacturerExt(ManufacturerOverviewModel model)
        {
            return View("SmartStore.Plugin.Import.eNVenta.Views.NVWidget._ManufacturerExt", model);
        }
    }

Damit die Logik jetzt sauber funktioniert, braucht es einer zusätzlichen Überladung für den Widget-Html-Helper:

public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object contextModel)
        {
            if (widgetZone.HasValue())
            {
                var widgetSelector = EngineContext.Current.Resolve<IWidgetSelector>();
                var widgets = widgetSelector.GetWidgets(widgetZone, contextModel);

                if (widgets.Any())
                {
                    var result = helper.Action("WidgetsByZone", "Widget", new { widgets = widgets, model = contextModel });
                    return result;
                }
            }

            return MvcHtmlString.Create("");
        }

Der Helper steuert den Widget-Controller an, welcher wiederum den IWidgetSelector, repective DefaultWidgetSelector benutzt, welches deshalb ebenfalls um das contextModel erweitert wird:

 public virtual IEnumerable<WidgetRouteInfo> GetWidgets(string widgetZone, object contextModel)
 {
           // .... alle aufrufe von GetDisplayWidgetRoute erweitern

           widget.GetDisplayWidgetRoute(widgetZone, contextModel, out actionName, out controllerName, out routeValues);
           // .... 	
           simpleWidget.GetDisplayWidgetRoute(widgetZone, contextModel, out actionName, out controllerName, out routeValues);
}

An den benötigten Stellen die Überladung für das Model aufrufen, bei mir zum Testen im View ProductTemplate.Simple.cshtml eine Erweiterung für die Hersteller-Angabe:

<div class="manufacturer-pics">

    @foreach (var manufacturer in Model.Manufacturers.Where(x => x.PictureModel != null))
    {
        <div class="manufacturer-item">
            <div class="picture">
                <a href="@Url.RouteUrl("Manufacturer", new { SeName = manufacturer.SeName })" title="@manufacturer.PictureModel.Title">
                    <img alt="@manufacturer.PictureModel.AlternateText" src="@manufacturer.PictureModel.ImageUrl" title="@manufacturer.PictureModel.Title" /></a>
            </div>
        </div>
        @Html.Widget("productdetails_manufacturer_after", manufacturer)
    }

</div>

Der größte Nachteil der Lösung ist die Anpassung des IWidget-Interface, alles andere sollte keine Abhängigkeiten verletzen. Als Ergebnis bekommt man dafür die Möglichkeit, Context-Abhängige Widgets in seinem Plugin zu schreiben, eine meines Erachtens saubere Übergabe des Contextes ohne zusätzliche Abhängigkeiten. In dem Widget-Controller lassen sich zusätzliche Daten ermitteln oder das Context-Model in ein eigenes "WidgetModel" überführen. Vielleicht ist das ein Ansatz, den ihr für die Weiterentwicklung berücksichtigen könnt.

 

Viele Grüße Holger



#11 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 30 May 2014 - 19:13

Hmm, interessant. Einziger Wermutstropen ist: das Plugin-Projekt (in welchem ja das Widget sitzt) braucht immer noch einen Verweis zu SmartStore.Web. Sonst wäre der Model-Typ nicht bekannt. Aber... egal, solange der Verweis nicht mandatorisch ist. Man muss das Parent-Model ja nicht verwenden.

 

Also, ich werde das (vielleicht gleich noch, spät. Montag) implementieren  ;)

 

Danke und weiter so.


Murat Cakir
SmartStore AG


#12 Murat Cakir

Murat Cakir

    SmartStore AG

  • Administrators
  • 1118 Beiträge

Geschrieben: 30 May 2014 - 20:26

Bitte siehe Commit 34ae0056a31d.


Murat Cakir
SmartStore AG


#13 NissenVelten

NissenVelten

    Newbie

  • Members
  • Punkt
  • 7 Beiträge

Geschrieben: 02 June 2014 - 09:09

Find ich sehr gut, dass die Integration so schnell möglich war. Besonders gut find ich auch den Rückfall auf das View-Model wenn das Model nicht explizit angegeben wurde. GJ :cool: