Error executing template "Designs/Swift/Paragraph/Swift_ProductDetailsImage.cshtml"
System.InvalidOperationException: Nullable object must have a value.
   at System.Nullable`1.get_Value()
   at CompiledRazorTemplates.Dynamic.RazorEngine_3039c4d67ac34667990d3d7a91a399fe.ExecuteAsync()
   at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader)
   at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer)
   at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag)
   at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
   at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
   at Dynamicweb.Rendering.Template.RenderRazorTemplate()

1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Ecommerce.ProductCatalog 3 @using Dynamicweb.Frontend 4 @using System.IO 5 @using System.Text.RegularExpressions; 6 7 @functions { 8 public ProductViewModel product { get; set; } = new ProductViewModel(); 9 public string galleryLayout { get; set; } 10 public string[] supportedImageFormats { get; set; } 11 public string[] supportedVideoFormats { get; set; } 12 public string[] supportedDocumentFormats { get; set; } 13 public string[] allSupportedFormats { get; set; } 14 15 public class RatioSettings 16 { 17 public string Ratio { get; set; } 18 public string CssClass { get; set; } 19 public string CssVariable { get; set; } 20 public string Fill { get; set; } 21 } 22 23 public RatioSettings GetRatioSettings(string size = "desktop") 24 { 25 var ratioSettings = new RatioSettings(); 26 27 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", ""); 28 ratio = ratio != "0" ? ratio : ""; 29 string cssClass = ratio != "" && ratio != "fill" ? " ratio" : ""; 30 string cssVariable = ratio != "" && ratio != "fill" ? "--bs-aspect-ratio: " + ratio : ""; 31 cssClass = ratio == "fill" && size == "mobile" ? " ratio" : cssClass; 32 cssVariable = ratio == "fill" && size == "mobile" ? "--bs-aspect-ratio: 66%" : cssVariable; 33 34 ratioSettings.Ratio = ratio; 35 ratioSettings.CssClass = cssClass; 36 ratioSettings.CssVariable = cssVariable; 37 ratioSettings.Fill = ratio == "fill" ? " h-100" : ""; 38 39 return ratioSettings; 40 } 41 42 public string GetArrowsColor() 43 { 44 var invertColor = Model.Item.GetBoolean("InvertModalArrowsColor"); 45 var arrowsColor = invertColor ? " carousel-dark" : string.Empty; 46 return arrowsColor; 47 } 48 49 public string GetThumbnailPlacement() 50 { 51 return Model.Item.GetRawValueString("ThumbnailPlacement", "bottom"); 52 } 53 54 public string GetThumbnailRowSettingCss() 55 { 56 switch (GetThumbnailPlacement()) 57 { 58 case "bottom": 59 return "d-flex flex-wrap"; 60 case "left": 61 return "d-flex flex-column order-first"; 62 case "right": 63 return "d-flex flex-column order-last"; 64 default: 65 return "d-flex flex-wrap"; 66 } 67 } 68 69 public Dictionary<string, object> GetVideoParams(MediaViewModel asset, string size) 70 { 71 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Name; 72 string type = GetVideoType(asset.Value); 73 bool openInModal = Model.Item.GetString("OpenVideoInModal") == "true" ? true : false; 74 bool autoPlay = Model.Item.GetBoolean("VideoAutoPlay"); 75 76 var videoParams = new Dictionary<string, object>(); 77 videoParams.Add("AssetName", asset.Name); 78 videoParams.Add("AssetVideoType", type); 79 videoParams.Add("AssetDisplayName", asset.DisplayName); 80 videoParams.Add("OpenVideoInModal", openInModal); 81 videoParams.Add("VideoAutoPlay", autoPlay); 82 videoParams.Add("Size", size); 83 videoParams.Add("Id", Model.ID); 84 return videoParams; 85 86 } 87 88 public string GetVideoType(string assetValue) 89 { 90 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : string.Empty; 91 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type; 92 type = string.IsNullOrEmpty(type) ? "selfhosted" : type; 93 94 return type; 95 } 96 97 public string GetYoutubeScreenDump(string assetValue, string quality) 98 { 99 var regex = new Regex(@"(?:youtube\.com\/.*[\?&]v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]+)(?:\?.*)?"); 100 Match match = regex.Match(assetValue); 101 string videoId = match.Success ? match.Groups[1].Value : string.Empty; 102 string youtubeThumbnail = $"https://img.youtube.com/vi/{videoId}/{quality}.jpg"; 103 return youtubeThumbnail; 104 } 105 } 106 107 @{ 108 ProductViewModel product = null; 109 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails")) 110 { 111 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 112 } 113 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode) 114 { 115 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page); 116 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel(); 117 118 if (productList?.Products is object) 119 { 120 product = productList.Products[0]; 121 } 122 } 123 } 124 125 @if (product is object) 126 { 127 @* Supported formats *@ 128 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" }; 129 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" }; 130 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" }; 131 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray(); 132 133 @* Collect the assets *@ 134 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue()?.OfType<string>().ToList() ?? new List<string>(); 135 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages"); 136 137 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@ 138 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : ""; 139 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories != null 140 ? product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets ?? Enumerable.Empty<MediaViewModel>()) 141 : Enumerable.Empty<MediaViewModel>(); 142 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage)); 143 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { }; 144 assetsList = assetsList.Union(assetsImages); 145 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages ?? Enumerable.Empty<MediaViewModel>()) : assetsList; 146 assetsList = includeImagePatternImages && assetsList.Count() == 0 && product.DefaultImage != null ? assetsList.Append(product.DefaultImage) : assetsList; 147 148 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback"); 149 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage"); 150 151 int totalAssets = 0; 152 if (showOnlyPrimaryImage == false) 153 { 154 foreach (MediaViewModel asset in assetsList) 155 { 156 var assetValue = asset.Value; 157 foreach (string format in allSupportedFormats) 158 { 159 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 160 { 161 totalAssets++; 162 } 163 } 164 } 165 } 166 167 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback) 168 { 169 assetsList = new List<MediaViewModel>() { product.DefaultImage }; 170 totalAssets = 1; 171 } 172 173 @* Theme settings *@ 174 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : ""; 175 176 var badgeParms = new Dictionary<string, object>(); 177 badgeParms.Add("size", "h5"); 178 badgeParms.Add("saleBadgeType", Model.Item.GetRawValue("SaleBadgeType")); 179 badgeParms.Add("saleBadgeCssClassName", Model.Item.GetRawValue("SaleBadgeDesign")); 180 badgeParms.Add("newBadgeCssClassName", Model.Item.GetRawValue("NewBadgeDesign")); 181 badgeParms.Add("newPublicationDays", Model.Item.GetInt32("NewPublicationDays")); 182 badgeParms.Add("campaignBadgesValues", Model.Item.GetList("CampaignBadges")?.GetRawValue().OfType<string>().ToList()); 183 184 bool saleBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("SaleBadgeDesign")) && Model.Item.GetRawValueString("SaleBadgeDesign") != "none" ? true : false; 185 bool newBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("NewBadgeDesign")) && Model.Item.GetRawValueString("NewBadgeDesign") != "none" ? true : false; 186 DateTime createdDate = product.Created.Value; 187 bool showBadges = saleBadgeEnabled && product.Discount.Price != 0 ? true : false; 188 showBadges = (newBadgeEnabled && Model.Item.GetInt32("NewPublicationDays") == 0) || (newBadgeEnabled && (createdDate.AddDays(Model.Item.GetInt32("NewPublicationDays")) > DateTime.Now)) ? true : showBadges; 189 showBadges = !string.IsNullOrEmpty(Model.Item.GetRawValueString("CampaignBadges")) ? true : showBadges; 190 191 @* Get assets from selected categories or get all assets *@ 192 if (totalAssets != 0) 193 { 194 int assetNumber = 0; 195 int thumbnailNumber = 0; 196 int modalAssetNumber = 0; 197 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty; 198 199 <div class="d-flex gap-3 h-100 @(thumbnailAxisCss) @(theme) item_@Model.Item.SystemName.ToLower()"> 200 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel"> 201 <div class="carousel-inner h-100"> 202 @foreach (MediaViewModel asset in assetsList) 203 { 204 var assetValue = asset.Value; 205 foreach (string format in allSupportedFormats) 206 { 207 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 208 { 209 string activeSlide = assetNumber == 0 ? "active" : ""; 210 211 <div class="carousel-item @activeSlide" data-bs-interval="99999"> 212 @{ 213 string size = "mobile"; 214 215 string imageTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ImageTheme")) ? " theme " + Model.Item.GetRawValueString("ImageTheme").Replace(" ", "").Trim().ToLower() : ""; 216 217 218 <div class="h-100 @(imageTheme)"> 219 @foreach (string imageFormat in supportedImageFormats) 220 { //Images 221 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 222 { 223 if (product is object) 224 { 225 string productName = product.Name; 226 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 227 string imageLinkPath = Uri.EscapeDataString(imagePath); 228 229 RatioSettings ratioSettings = GetRatioSettings(size); 230 231 var parms = new Dictionary<string, object>(); 232 parms.Add("alt", productName + asset.Keywords); 233 parms.Add("itemprop", "image"); 234 parms.Add("columns", Model.GridRowColumnCount); 235 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading")); 236 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage")); 237 if (!string.IsNullOrEmpty(asset.DisplayName)) 238 { 239 parms.Add("title", asset.DisplayName); 240 } 241 242 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 243 { 244 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 245 } 246 else 247 { 248 parms.Add("cssClass", "mw-100 mh-100"); 249 } 250 251 <a href="@imagePath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 252 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber"> 253 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 254 </div> 255 </a> 256 } 257 } 258 } 259 @foreach (string videoFormat in supportedVideoFormats) 260 { //Videos 261 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 262 { 263 if (Model.Item.GetString("OpenVideoInModal") == "true") 264 { 265 if (product is object) 266 { 267 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 268 269 string productName = product.Name; 270 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 271 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : ""; 272 273 RatioSettings ratioSettings = GetRatioSettings(size); 274 275 string type = GetVideoType(asset.Value); 276 277 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "maxresdefault") : string.Empty; 278 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath; 279 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : ""; 280 281 282 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 283 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber"> 284 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 285 @if (type != "selfhosted") 286 { 287 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@productName" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;"> 288 } 289 else 290 { 291 string videoType = Path.GetExtension(asset.Value).ToLower(); 292 293 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 294 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")"> 295 </video> 296 } 297 </div> 298 </div> 299 300 } 301 } 302 else 303 { 304 if (product is object) 305 { 306 var videoParams = GetVideoParams(asset, size); 307 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value}, videoParams); 308 309 } 310 } 311 } 312 } 313 @foreach (string documentFormat in supportedDocumentFormats) 314 { //Documents 315 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 316 { 317 if (product is object) 318 { 319 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 320 321 string productName = product.Name; 322 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 323 string imageLinkPath = Uri.EscapeDataString(imagePath); 324 325 RatioSettings ratioSettings = GetRatioSettings(size); 326 327 var parms = new Dictionary<string, object>(); 328 parms.Add("alt", productName + asset.Keywords); 329 parms.Add("itemprop", "image"); 330 parms.Add("fullwidth", true); 331 parms.Add("columns", Model.GridRowColumnCount); 332 if (!string.IsNullOrEmpty(asset.DisplayName)) 333 { 334 parms.Add("title", asset.DisplayName); 335 } 336 337 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 338 { 339 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 340 } 341 else 342 { 343 parms.Add("cssClass", "mw-100 mh-100"); 344 } 345 346 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@Translate("Download")"> 347 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 348 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 349 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0) 350 { 351 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 352 } 353 </div> 354 </a> 355 } 356 357 } 358 } 359 </div> 360 } 361 362 363 </div> 364 assetNumber++; 365 } 366 } 367 } 368 </div> 369 @if (showBadges) 370 { 371 <div class="position-absolute top-0 left-0 p-2 p-lg-3"> 372 @{@RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)} 373 </div> 374 } 375 376 </div> 377 378 @if (totalAssets > 1) 379 { 380 <div class="@(GetThumbnailRowSettingCss()) gap-3" id="SmallScreenImagesThumbnails_@Model.ID"> 381 @foreach (MediaViewModel asset in assetsList) 382 { 383 var assetValue = asset.Value; 384 string assetName = asset.Name; 385 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 386 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : null; 387 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 388 389 string imagePath = Dynamicweb.Context.Current.Server.UrlEncode(assetValue); 390 imagePath = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "https://img.youtube.com/vi/" + assetValue.Substring(assetValue.LastIndexOf('/') + 1) + "/mqdefault.jpg" : imagePath; 391 string imagePathThumb = assetValue.StartsWith("/Files/", StringComparison.OrdinalIgnoreCase) ? imagePath.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0 && imagePath.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0 ? $"/Admin/Public/GetImage.ashx?image={imagePath}&width=180&format=webp" : imagePath : assetValue; 392 393 RatioSettings ratioSettings = GetRatioSettings("desktop"); 394 395 <div class="border outline-none @(ratioSettings.CssClass)" style="@(ratioSettings.CssVariable); cursor: pointer; " data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber"> 396 @foreach (string imageFormat in supportedImageFormats) 397 { //Images 398 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 399 { 400 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 w-100 h-100" style="object-fit: contain;"> 401 402 thumbnailNumber++; 403 } 404 } 405 406 @foreach (string videoFormat in supportedVideoFormats) 407 { //Videos 408 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 409 { 410 411 string type = GetVideoType(asset.Value); 412 413 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "mqdefault") : ""; 414 videoScreendumpPath = type == "vimeo" ? string.Empty : videoScreendumpPath; 415 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath; 416 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : string.Empty; 417 418 <div class="icon-4 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 419 420 if (type != "selfhosted") 421 { 422 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@assetTitle" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;" /> 423 } 424 else 425 { 426 string videoType = Path.GetExtension(asset.Value).ToLower(); 427 428 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 429 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")"> 430 </video> 431 } 432 433 thumbnailNumber++; 434 } 435 } 436 437 @foreach (string documentFormat in supportedDocumentFormats) 438 { //Documents 439 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 440 { 441 <a href="@Uri.EscapeDataString(assetValue)" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 7rem; max-width: 8rem;" download title="@asset.Value"> 442 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0) 443 { 444 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 445 <div class="icon-3 position-absolute text-light" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 446 </div> 447 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 mw-100 mh-100" style="object-fit: cover;"> 448 } 449 else 450 { 451 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 452 <div class="icon-3 position-absolute" style="z-index: 1">@ReadFile(iconPath + "file-text.svg")</div> 453 </div> 454 } 455 </a> 456 457 thumbnailNumber++; 458 } 459 } 460 </div> 461 } 462 </div> 463 } 464 </div> 465 466 @* Modal with slides *@ 467 <div class="modal fade swift_products-details-images-modal" id="modal_@Model.ID" tabindex="-1" aria-labelledby="productDetailsGalleryModalTitle_@Model.ID" aria-hidden="true"> 468 <div class="modal-dialog modal-dialog-centered modal-xl"> 469 <div class="modal-content"> 470 <div class="modal-header visually-hidden"> 471 <h5 class="modal-title" id="productDetailsGalleryModalTitle_@Model.ID">@product.Title</h5> 472 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 473 </div> 474 <div class="modal-body p-2 p-lg-3 h-100"> 475 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel"> 476 <div class="carousel-inner h-100 @theme"> 477 @foreach (MediaViewModel asset in assetsList) 478 { 479 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 480 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray()) 481 { 482 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0) 483 { 484 string imagePath = assetValue; 485 string activeSlide = modalAssetNumber == 0 ? "active" : ""; 486 487 var parms = new Dictionary<string, object>(); 488 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto"); 489 parms.Add("fullwidth", true); 490 parms.Add("columns", Model.GridRowColumnCount); 491 492 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999"> 493 @foreach (string imageFormat in supportedImageFormats) 494 { //Images 495 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 496 { 497 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 498 } 499 } 500 501 @foreach (string videoFormat in supportedVideoFormats) 502 { //Videos 503 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 504 { 505 var videoParams = GetVideoParams(asset, "modal"); 506 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value }, videoParams) 507 } 508 } 509 </div> 510 modalAssetNumber++; 511 } 512 } 513 } 514 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev"> 515 <span class="carousel-control-prev-icon" aria-hidden="true"></span> 516 <span class="visually-hidden">@Translate("Previous")</span> 517 </button> 518 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next"> 519 <span class="carousel-control-next-icon" aria-hidden="true"></span> 520 <span class="visually-hidden">@Translate("Next")</span> 521 </button> 522 </div> 523 </div> 524 </div> 525 </div> 526 </div> 527 </div> 528 } 529 else if (Pageview.IsVisualEditorMode) 530 { 531 RatioSettings ratioSettings = GetRatioSettings("desktop"); 532 533 <div class="h-100 @theme"> 534 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)"> 535 <img src="/Files/Images/missing_image.jpg" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;"> 536 </div> 537 </div> 538 } 539 } 540 else if (Pageview.IsVisualEditorMode) 541 { 542 <div class="alert alert-dark m-0">@Translate("No products available")</div> 543 } 544 545 546 547

Error executing template "Designs/Swift/Paragraph/Swift_ProductAddToCart.cshtml"
System.NullReferenceException: Object reference not set to an instance of an object.
   at CompiledRazorTemplates.Dynamic.RazorEngine_29a890a05fd94c8cacab243516e48dd3.ExecuteAsync()
   at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader)
   at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer)
   at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag)
   at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
   at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
   at Dynamicweb.Rendering.Template.RenderRazorTemplate()

1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using CustomCode.Helpers 3 @using Dynamicweb.Ecommerce.ProductCatalog 4 @using Dynamicweb.Core.Encoders 5 @using System.Globalization 6 7 @functions { 8 string DoubleToString(double? value) 9 { 10 if (value.HasValue) 11 { 12 return value.Value.ToString(CultureInfo.InvariantCulture); 13 } 14 return null; 15 } 16 } 17 18 @{ 19 ProductViewModel product = null; 20 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails")) 21 { 22 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 23 } 24 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode) 25 { 26 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page); 27 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel(); 28 29 if (productList?.Products is object) 30 { 31 product = productList.Products[0]; 32 } 33 } 34 35 string anonymousUsersLimitations = Pageview.AreaSettings.GetRawValueString("AnonymousUsers", ""); 36 bool anonymousUser = Pageview.User == null; 37 bool isErpConnectionDown = !Dynamicweb.Core.Converter.ToBoolean(Dynamicweb.Context.Current.Items["IsWebServiceConnectionAvailable"]); 38 bool hideAddToCart = anonymousUsersLimitations.Contains("cart") && anonymousUser || Pageview.AreaSettings.GetBoolean("ErpDownHideAddToCart") && isErpConnectionDown; 39 hideAddToCart = Pageview.IsVisualEditorMode ? false : hideAddToCart; 40 } 41 42 @if (product is object && !hideAddToCart) 43 { 44 string contextCart = Model.Item.GetRawValueString("ContextCart", ""); 45 if (string.IsNullOrEmpty(contextCart) && Pageview.CurrentParagraph?.Item != null) 46 { 47 contextCart = Pageview.CurrentParagraph.Item["ContextCart"]?.ToString() ?? ""; 48 } 49 if (string.IsNullOrEmpty(contextCart) && Pageview.Page?.Item != null) 50 { 51 contextCart = Pageview.Page.Item["ContextCart"]?.ToString() ?? ""; 52 } 53 if (string.IsNullOrEmpty(contextCart)) 54 { 55 int cartRootPageId = GetPageIdByNavigationTag("CartRootPage"); 56 if (cartRootPageId > 0) 57 { 58 var cartRootPage = Dynamicweb.Content.Services.Pages?.GetPage(cartRootPageId); 59 if (cartRootPage != null && cartRootPage.Item != null) 60 { 61 contextCart = cartRootPage.Item["ContextCart"]?.ToString() ?? ""; 62 } 63 } 64 } 65 if (string.IsNullOrEmpty(contextCart)) 66 { 67 int cartServicePageId = GetPageIdByNavigationTag("CartService"); 68 if (cartServicePageId > 0) 69 { 70 var cartServicePage = Dynamicweb.Content.Services.Pages?.GetPage(cartServicePageId); 71 if (cartServicePage != null && cartServicePage.Item != null) 72 { 73 contextCart = cartServicePage.Item["ContextCart"]?.ToString() ?? ""; 74 } 75 } 76 } 77 78 string horizontalAlign = Model.Item.GetRawValueString("HorizontalAlignment", ""); 79 horizontalAlign = horizontalAlign == "center" ? "justify-content-center" : horizontalAlign; 80 horizontalAlign = horizontalAlign == "end" ? "justify-content-end" : horizontalAlign; 81 horizontalAlign = horizontalAlign == "full" ? "" : horizontalAlign; 82 83 bool favoritesSelector = !string.IsNullOrEmpty(Model.Item.GetString("ShowAddToFavorites")) ? Model.Item.GetBoolean("ShowAddToFavorites") : false; 84 bool quantitySelector = !string.IsNullOrEmpty(Model.Item.GetString("ShowQuantitySelector")) ? Model.Item.GetBoolean("ShowQuantitySelector") : false; 85 bool unitsSelector = !string.IsNullOrEmpty(Model.Item.GetString("ShowUnitsSelector")) ? Model.Item.GetBoolean("ShowUnitsSelector") : false; 86 bool hideInventory = !string.IsNullOrEmpty(Model.Item.GetString("HideInventory")) ? Model.Item.GetBoolean("HideInventory") : false; 87 bool hideStockState = !string.IsNullOrEmpty(Model.Item.GetString("HideStockState")) ? Model.Item.GetBoolean("HideStockState") : false; 88 89 string buttonSize = Model.Item.GetRawValueString("ButtonSize", "regular"); 90 string inputSize = string.Empty; 91 92 switch (buttonSize) 93 { 94 case "small": 95 inputSize = " input-group-sm"; 96 buttonSize = " btn-sm"; 97 break; 98 case "regular": 99 buttonSize = string.Empty; 100 break; 101 case "large": 102 inputSize = " input-group-lg"; 103 buttonSize = " btn-lg"; 104 break; 105 } 106 107 string iconPath = "/Files/icons/"; 108 string url = "/Default.aspx?ID=" + (GetPageIdByNavigationTag("CartService")); 109 if (!url.Contains("LayoutTemplate")) 110 { 111 url += url.Contains("?") ? "&LayoutTemplate=Swift_MiniCart.cshtml" : "?LayoutTemplate=Swift_MiniCart.cshtml"; 112 } 113 114 string whenVariantsExist = Model.Item.GetRawValueString("WhenVariantsExist", "hide"); 115 string flexFill = Model.Item.GetRawValueString("HorizontalAlignment", "") == "full" ? "flex-fill" : ""; 116 string fullWidth = Model.Item.GetRawValueString("HorizontalAlignment", "") == "full" ? "w-100" : ""; 117 string addToCartIcon = Model.Item.GetRawValueString("Icon", iconPath + "shopping-cart.svg"); 118 string requestQuoteIcon = Model.Item.GetRawValueString("RequestQuoteIcon", iconPath + "envelope.svg"); 119 120 string addToCartLabel = !addToCartIcon.Contains("_none") ? $"<span >{ReadFile(addToCartIcon)}</span>" : ""; 121 122 requestQuoteIcon = !requestQuoteIcon.Contains("_none") ? $"<span>{ReadFile(requestQuoteIcon)}</span>" : ""; 123 124 addToCartLabel += !addToCartIcon.Contains("_none") && !Model.Item.GetBoolean("HideButtonText") ? " " : ""; 125 addToCartLabel += !Model.Item.GetBoolean("HideButtonText") ? $"<span class=\"d-none d-md-inline\">{Translate("Add to cart")}</span><span class=\"d-inline d-md-none\">{Translate("Add")}</span>" : ""; 126 bool isLazyLoadingForProductInfoEnabled = Dynamicweb.Core.Converter.ToBoolean(Dynamicweb.Context.Current.Items["IsLazyLoadingForProductInfoEnabled"]); 127 128 bool userHasPendingQuote = Dynamicweb.Ecommerce.Common.Context.Cart != null && Dynamicweb.Ecommerce.Common.Context.Cart.IsQuote; 129 130 if (product.VariantInfo?.VariantInfo == null || whenVariantsExist == "disable") 131 { 132 string unitId = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.Form.Get("UnitId")) ? Dynamicweb.Context.Current.Request.Form.Get("UnitId") : product.DefaultUnitId; 133 if (string.IsNullOrEmpty(unitId) && product?.UnitOptions != null) 134 { 135 if (product.UnitOptions.FirstOrDefault<UnitOptionViewModel>() != null) 136 { 137 unitId = product.UnitOptions.FirstOrDefault<UnitOptionViewModel>().Id; 138 } 139 } 140 141 double? stepQty = product.PurchaseQuantityStep > 0 ? product.PurchaseQuantityStep : 1; 142 double? minQty = product.PurchaseMinimumQuantity > 0 ? product.PurchaseMinimumQuantity : 1; 143 double? valueQty = minQty > stepQty ? minQty : stepQty; 144 string disableAddToCart = null; 145 double? maxQty = null; 146 147 148 disableAddToCart = whenVariantsExist == "disable" && product.VariantInfo?.VariantInfo != null && string.IsNullOrEmpty(product.VariantId) ? "disabled" : disableAddToCart; 149 disableAddToCart = product.Discontinued ? "disabled" : disableAddToCart; 150 151 //Capo custom - all products that are equal to SalesBlocked_[CountryCode] = true should not be possible to purchase (disableAddToCart = "disabled") 152 var areaEcomCountryCode = Dynamicweb.Frontend.PageView.Current()?.Area?.EcomCountryCode; 153 bool isSalesBlocked = false; 154 if (areaEcomCountryCode != null && product.ProductFields.TryGetValue("SalesBlocked_" + areaEcomCountryCode?.ToUpper(), out var salesBlockedValue) ) 155 { 156 157 if (salesBlockedValue != null && bool.TryParse(salesBlockedValue.ToString(), out bool parsedValue)) 158 { 159 isSalesBlocked = parsedValue; 160 } 161 162 if (isSalesBlocked && disableAddToCart != "disabled") 163 { 164 disableAddToCart = "disabled"; 165 } 166 } 167 168 bool isQuoteRequest = QuoteRequestHelper.IsQuoteRequest(product, areaEcomCountryCode); 169 170 bool hasVariants = product.VariantInfo?.VariantInfo != null && string.IsNullOrEmpty(product.VariantId); 171 bool isProductList = Dynamicweb.Context.Current.Items.Contains("IsProductList") && Dynamicweb.Core.Converter.ToBoolean(Dynamicweb.Context.Current.Items["IsProductList"]); 172 173 if (unitsSelector && product.UnitOptions?.Count > 0) 174 { 175 <form method="post" action="/Default.aspx?ID=@(Pageview.Page.ID)&ProductId=@product.Id" id="UnitSelectorForm_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")_@Model.ID"> 176 <input type="hidden" name="redirect" value="false"> 177 <input type="hidden" name="VariantID" value="@(product.VariantId ?? "")"> 178 <input type="hidden" name="UnitID" class="js-unit-id" value="@unitId"> 179 </form> 180 } 181 <div class="d-flex @horizontalAlign @fullWidth js-input-group item_@Model.Item.SystemName.ToLower()"> 182 @if (!anonymousUser && favoritesSelector) 183 { 184 @RenderPartial("Components/ToggleFavorite.cshtml", product) 185 } 186 187 @if (hasVariants && isProductList) 188 { 189 int shopPageId = GetPageIdByNavigationTag("Shop"); 190 string productDetailLink = shopPageId > 0 191 ? product.GetProductLink(shopPageId, false) 192 : Dynamicweb.Context.Current.Request.Url.AbsoluteUri; 193 194 <a href="@productDetailLink" 195 class="btn btn-primary@(buttonSize) @flexFill @fullWidth js-view-variant-details" 196 style="position: relative; z-index: 1; white-space: nowrap" 197 id="ViewVariantDetailsButton@(product.Id)_@Pageview.CurrentParagraph.ID" 198 title="@Translate("View details")"> 199 @if (!Model.Item.GetBoolean("HideButtonText")) 200 { 201 <span class="text-nowrap d-flex align-items-center justify-content-center gap-2"> 202 @Translate("View details") 203 </span> 204 } 205 else 206 { 207 <span>@ReadFile(iconPath + "chevron-right.svg")</span> 208 } 209 </a> 210 } 211 212 else if (isQuoteRequest) 213 { 214 <div class="quote-request"> 215 @{ 216 int shopPageId = GetPageIdByNavigationTag("Shop"); 217 string productUrl = isProductList && shopPageId > 0 218 ? product.GetProductLink(shopPageId, false) 219 : Dynamicweb.Context.Current.Request.Url.AbsoluteUri; 220 } 221 222 <button type="button" 223 class="btn btn-primary btn-sm js-view-variant-details" 224 style="position: relative; z-index: 1" 225 data-bs-toggle="modal" 226 data-bs-target="#quoteRequestModal" 227 aria-label="@Translate("Quote request")" 228 data-product-name="@product.Name" 229 data-product-number="@product.Number" 230 data-product-url="@productUrl"> 231 @if (Model.Item.GetBoolean("HideButtonText")) 232 { 233 @requestQuoteIcon 234 } 235 else 236 { 237 @Translate("Quote request") 238 } 239 </button> 240 </div> 241 } 242 else 243 { 244 <form method="post" action="@url" class="@fullWidth" style="z-index: 1"> 245 <input type="hidden" name="OrderContext" value="@contextCart"> 246 <input type="hidden" name="minicartid" value="@contextCart"> 247 <input type="hidden" name="redirect" value="false"> 248 <input type="hidden" name="ProductId" value="@product.Id"> 249 <input type="hidden" name="ProductName" value="@HtmlEncoder.HtmlEncode(product.Name)"> 250 <input type="hidden" name="ProductVariantName" value="@product.VariantName"> 251 <input type="hidden" name="ProductCurrency" value="@Dynamicweb.Ecommerce.Common.Context.Currency.Code"> 252 <input type="hidden" name="ProductPrice" value="@product.Price.ToStringInvariant()"> 253 <input type="hidden" name="ProductDiscount" value="@product.Discount.ToStringInvariant()"> 254 <input type="hidden" name="ProductReferer" value="component_ProductAddToCart"> 255 <input type="hidden" name="cartcmd" value="add"> 256 <input type="submit" class="d-none" onclick="event.preventDefault(); swift.Cart.Update(event)"> @* Fix for enterKey should not redirect to minicart page *@ 257 258 @if (!string.IsNullOrEmpty(product.VariantId)) 259 { 260 <input type="hidden" name="VariantId" value="@product.VariantId"> 261 } 262 263 <template class="js-step-quantity-warning"> 264 <div class="modal-header"> 265 <h1 class="modal-title fs-5">@Translate("The quantity is not valid")</h1> 266 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 267 </div> 268 <div class="modal-body"> 269 @Translate("Please select a quantity that is dividable by") @stepQty 270 </div> 271 </template> 272 273 274 <template class="js-min-quantity-warning"> 275 <div class="modal-header"> 276 <h1 class="modal-title fs-5">@Translate("The product could not be added to the cart")</h1> 277 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 278 </div> 279 <div class="modal-body"> 280 @Translate("The quantity is not valid. You must buy at least") @product.PurchaseMinimumQuantity 281 </div> 282 </template> 283 284 <template class="js-value-missing-warning"> 285 <div class="modal-header"> 286 <h1 class="modal-title fs-5">@Translate("No amount specified")</h1> 287 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 288 </div> 289 <div class="modal-body"> 290 @Translate("Specify an amount to add to the cart") 291 </div> 292 </template> 293 294 295 @if (userHasPendingQuote) 296 { 297 <input type="hidden" name="PendingQuote" value="true"> 298 299 <template class="js-pending-quote-notice"> 300 <div class="modal-header"> 301 <h1 class="modal-title fs-5">@Translate("Pending Quote")</h1> 302 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@Translate("Close")"></button> 303 </div> 304 <div class="modal-body"> 305 @Translate("You need to complete your current quote or empty the cart before adding this product to cart.") 306 </div> 307 </template> 308 } 309 310 @if (quantitySelector || (!anonymousUser && product.VariantInfo?.VariantInfo != null) || (!anonymousUser && favoritesSelector)) 311 { 312 <input type="hidden" id="Unit_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")" name="UnitID" value="@unitId" /> 313 } 314 315 <div class="d-flex flex-row w-100"> 316 @if (!quantitySelector) 317 { 318 <input id="Quantity_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")" class="swift_quantity_field" name="Quantity" value="@valueQty" type="hidden" @disableAddToCart> 319 } 320 321 @if (unitsSelector && product.UnitOptions != null && product.UnitOptions.Count > 0) 322 { 323 string selectedUnitName = !string.IsNullOrEmpty(unitId) && product?.UnitOptions != null ? unitId : product.UnitOptions.FirstOrDefault<UnitOptionViewModel>().Name; 324 325 foreach (var unitOption in product.UnitOptions) 326 { 327 if (unitOption.Id == unitId) 328 { 329 selectedUnitName = unitOption.Name; 330 } 331 } 332 333 <div class="d-flex flex-column gap-2 w-100"> 334 <div class="input-group input-primary-button-group flex-nowrap@(inputSize)"> 335 336 @if (quantitySelector) 337 { 338 <input id="Quantity_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")" name="Quantity" value="@DoubleToString(valueQty)" step="@DoubleToString(stepQty)" min="@DoubleToString(minQty)" max="@DoubleToString(maxQty)" class="form-control swift_quantity-field" style="min-width: 60px; max-width: 100px; z-index: 1" type="number" @disableAddToCart> 339 } 340 341 <button class="btn btn-secondary @flexFill dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> 342 @selectedUnitName 343 </button> 344 345 <ul class="dropdown-menu swift_unit-field"> 346 @foreach (var unitOption in product.UnitOptions) 347 { 348 var selectedUnit = unitOption.Id == unitId ? "selected" : ""; 349 350 <li> 351 <button type="button" class="btn dropdown-item" data-value="@unitOption.Id" onclick="document.querySelector('#UnitSelectorForm_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")_@Model.ID').querySelector('.js-unit-id').value = this.getAttribute('data-value'); 352 document.querySelector('#Unit_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")').value = this.getAttribute('data-value'); 353 swift.PageUpdater.Update(document.querySelector('#UnitSelectorForm_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")_@Model.ID'))"> 354 <span>@unitOption.Name</span> 355 <span> 356 @if (unitOption.StockLevel > 0 || unitOption.NeverOutOfStock) 357 { 358 if (!Model.Item.GetBoolean("HideInventory") && !unitOption.NeverOutOfStock) 359 { 360 <span class="small text-success">@unitOption.StockLevel @Translate("In stock")</span> 361 } 362 else 363 { 364 <span class="small text-success">@Translate("In stock")</span> 365 } 366 } 367 else 368 { 369 <span class="small text-danger">@Translate("Out of Stock")</span> 370 } 371 </span> 372 </button> 373 </li> 374 } 375 </ul> 376 </div> 377 <button type="button" onclick="swift.Cart.Update(event)" class="btn btn-primary @(buttonSize) js-add-to-cart-button" style="white-space: nowrap" @disableAddToCart title="@Translate("Add to cart")" id="AddToCartButton@(product.Id)_@Pageview.CurrentParagraph.ID"> 378 @if (!Model.Item.GetBoolean("HideButtonText")) 379 { 380 <span class="text-nowrap d-flex align-items-center justify-content-center gap-2"> 381 @addToCartLabel 382 </span> 383 } 384 else 385 { 386 @addToCartLabel 387 } 388 </button> 389 </div> 390 } 391 else 392 { 393 <div class="input-group input-primary-button-group flex-nowrap@(inputSize)"> 394 @if (quantitySelector) 395 { 396 <input id="Quantity_@(product.Id)_@(product.VariantId?.Replace(".", "_") ?? "")" name="Quantity" value="@DoubleToString(valueQty)" step="@DoubleToString(stepQty)" min="@DoubleToString(minQty)" max="@DoubleToString(maxQty)" class="form-control swift_quantity-field" style="min-width: 60px; max-width: 100px; z-index: 1" type="number" @disableAddToCart> 397 } 398 399 <button type="button" onclick="swift.Cart.Update(event)" class="btn btn-primary @(buttonSize) @flexFill js-add-to-cart-button" style="white-space: nowrap" @disableAddToCart title="@Translate("Add to cart")" id="AddToCartButton@(product.Id)_@Pageview.CurrentParagraph.ID"> 400 @if (!Model.Item.GetBoolean("HideButtonText")) 401 { 402 <span class="text-nowrap d-flex align-items-center justify-content-center gap-2"> 403 @addToCartLabel 404 </span> 405 } 406 else 407 { 408 @addToCartLabel 409 } 410 </button> 411 </div> 412 } 413 </div> 414 </form> 415 } 416 </div> 417 } 418 else if (whenVariantsExist == "modal") 419 { 420 string ButtonShape = Model.Item.GetRawValueString("VariantButtonShape", "square"); 421 string buttonAspectRatio = Model.Item.GetRawValueString("VariantImageAspectRatio", "56%"); 422 423 string buttonText = Translate("Select"); 424 string variantId = !string.IsNullOrWhiteSpace(product.VariantId) ? product.VariantId : (product.DefaultVariantId ?? ""); 425 426 string variantSelectorServicePageId = !string.IsNullOrEmpty(Model.Item.GetString("VariantSelectorServicePageId")) ? Model.Item.GetLink("VariantSelectorServicePageId").PageId.ToString() : ""; 427 variantSelectorServicePageId = variantSelectorServicePageId != "" ? variantSelectorServicePageId : GetPageIdByNavigationTag("VariantSelectorService").ToString(); 428 429 <div class="d-flex @horizontalAlign w-100 item_@Model.Item.SystemName.ToLower()"> 430 @if (!anonymousUser && favoritesSelector) 431 { 432 @RenderPartial("Components/ToggleFavorite.cshtml", product) 433 } 434 <form action="/Default.aspx?ID=@variantSelectorServicePageId" data-response-target-element="DynamicModalContent" data-preloader="inline" style="z-index: 1" class="@fullWidth"> 435 <input type="hidden" name="ProductID" value="@product.Id"> 436 <input type="hidden" name="VariantID" value="@variantId"> 437 <input type="hidden" name="QuantitySelector" value="@quantitySelector.ToString()"> 438 <input type="hidden" name="HideInventory" value="@hideInventory.ToString()"> 439 <input type="hidden" name="HideStockState" value="@hideStockState.ToString()"> 440 <input type="hidden" name="ButtonLayout" value="@ButtonShape"> 441 <input type="hidden" name="ButtonAspectRatio" value="@buttonAspectRatio"> 442 <input type="hidden" name="VariantSelectorServicePage" value="@variantSelectorServicePageId"> 443 <input type="hidden" name="ViewType" value="ModalContent"> 444 @if (isLazyLoadingForProductInfoEnabled) 445 { 446 @* If lazy loading is enabled, bypass it because we're loading a modal window, so render everything as if it was server-side *@ 447 <input type="hidden" name="getproductinfo" value="true"> 448 } 449 <button type="button" onclick="swift.PageUpdater.Update(event)" class="btn btn-primary@(buttonSize) @fullWidth" title="@Translate("Select")" data-bs-toggle="modal" data-bs-target="#DynamicModal" id="OpenVariantSelectorModal@(product.Id)_@Pageview.CurrentParagraph.ID">@buttonText</button> 450 </form> 451 </div> 452 } 453 } 454 else if (Pageview.IsVisualEditorMode) 455 { 456 <div class="alert alert-dark m-0">@Translate("No products available")</div> 457 } 458
Error executing template "Designs/Swift/Paragraph/Swift_ProductFieldDisplayGroupsTabs.cshtml"
System.ArgumentNullException: Value cannot be null. (Parameter 'source')
   at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)
   at System.Linq.Enumerable.Where[TSource](IEnumerable`1 source, Func`2 predicate)
   at CompiledRazorTemplates.Dynamic.RazorEngine_9846836bc8704556a41858467d7a5d27.ExecuteAsync()
   at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader)
   at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
   at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer)
   at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
   at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag)
   at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
   at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
   at Dynamicweb.Rendering.Template.RenderRazorTemplate()

1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Ecommerce.ProductCatalog 3 @using System.IO 4 5 @functions { 6 7 ProductViewModel GetProduct() 8 { 9 ProductViewModel product = null; 10 11 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails")) 12 { 13 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 14 } 15 16 return product; 17 } 18 19 List<FieldGroupViewModel> GetDisplayGroups(ProductViewModel product) 20 { 21 22 bool hideFieldsWithZeroValue = Model.Item.GetBoolean("HideFieldsWithZeroValue"); 23 var selectedDisplayGroupSystemName = Model.Item.GetList("DisplayGroups")?.GetRawValue().OfType<string>(); 24 var displayGroups = product.GetProductDisplayGroupFieldsByGroupSystemNames(selectedDisplayGroupSystemName, hideFieldsWithZeroValue); 25 26 return displayGroups.ToList(); 27 } 28 29 public static string FirstCharToUpper(string input) 30 { 31 return input[0].ToString().ToUpper() + input.Substring(1); 32 } 33 34 public class RatioSettings 35 { 36 public string Ratio { get; set; } 37 public string CssClass { get; set; } 38 public string CssVariable { get; set; } 39 public string Fill { get; set; } 40 } 41 42 public RatioSettings GetRatioSettings() 43 { 44 var ratioSettings = new RatioSettings(); 45 46 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", ""); 47 ratio = ratio != "0" ? ratio : ""; 48 string cssClass = ratio != "" && ratio != "fill" ? " ratio" : ""; 49 string cssVariable = ratio != "" && ratio != "fill" ? "--bs-aspect-ratio: " + ratio : ""; 50 51 ratioSettings.Ratio = ratio; 52 ratioSettings.CssClass = cssClass; 53 ratioSettings.CssVariable = cssVariable; 54 ratioSettings.Fill = ratio == "fill" ? " h-100" : ""; 55 56 return ratioSettings; 57 } 58 } 59 60 @{ 61 ProductViewModel product = GetProduct(); 62 63 if (product is object) 64 { 65 66 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : ""; 67 string contentPadding = Model.Item.GetRawValueString("ContentPadding", ""); 68 contentPadding = contentPadding == "none" ? string.Empty : contentPadding; 69 contentPadding = contentPadding == "small" ? " p-2 p-md-3" : contentPadding; 70 contentPadding = contentPadding == "large" ? " p-4 p-md-5" : contentPadding; 71 72 string size = "compact"; 73 string gapCss = size == "full" ? " gap-4" : " gap-2"; 74 75 var displayGroups = GetDisplayGroups(product); 76 77 var templateParams = new Dictionary<string, object> { 78 { "Groups", displayGroups }, 79 { "Size", size }, 80 { "HideGroupHeaders", Model.Item.GetBoolean("HideGroupHeaders") }, 81 { "HideTitle", Model.Item.GetBoolean("HideTitle") } 82 }; 83 84 string templateLayout = Model.Item.GetRawValueString("Layout", "list"); 85 string templateLayoutPath = $"Components/Specifications/{FirstCharToUpper(templateLayout)}.cshtml"; 86 87 string activeStyle = ""; 88 var display = ""; 89 string descriptionId = "description"; 90 string description = !string.IsNullOrWhiteSpace(product.LongDescription) ? product.LongDescription : product.ShortDescription; 91 bool hasDescription = !string.IsNullOrWhiteSpace(description); 92 var firstDisplayGroup = displayGroups.FirstOrDefault(g => g.Fields.Any()); 93 var firstGroupID = firstDisplayGroup != null ? firstDisplayGroup.SystemName : (hasDescription ? descriptionId : displayGroups.FirstOrDefault()?.SystemName ?? ""); 94 95 string[] imageFormats = new string[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".tif", ".tiff" }; 96 string[] supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", ".pptx", ".igs", ".ipt", ".sat", ".stp", ".dwg", ".dxf", ".dwf" }; 97 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/"; 98 var selectedDocumentCategories = Model.Item.GetList("DocumentAssets")?.GetRawValue()?.OfType<string>().ToList() ?? new List<string>(); 99 var documentAssets = product.AssetCategories.Where(x => selectedDocumentCategories.Contains(x.SystemName)).SelectMany(x => x.Assets).Where(x => !imageFormats.Contains(Path.GetExtension(x.Value), StringComparer.OrdinalIgnoreCase)).ToList(); 100 bool hasDocuments = documentAssets.Any(); 101 102 <div class="h-100@(theme)@(contentPadding) item_@Model.Item.SystemName.ToLower()"> 103 <div class="grid gap-0"> 104 <ul class="nav nav-tabs g-col-12"> 105 @foreach (var group in displayGroups) { 106 if (!group.Fields.Any()) { 107 display = "display:none"; 108 } 109 110 var idName = group.SystemName; 111 112 if (group.SystemName.ToLower() == firstGroupID.ToLower()) { 113 activeStyle = "active"; 114 } 115 else { 116 activeStyle = ""; 117 } 118 119 <li class="nav-item" style="@display"> 120 <a 121 class="nav-link @idName @activeStyle" 122 id="@idName-tab" 123 data-bs-toggle="tab" 124 href="#@idName" 125 role="tab" 126 aria-controls="@idName" 127 aria-selected="true" 128 > 129 <span class="">@Translate(group.Name)</span> 130 </a> 131 </li> 132 } 133 134 @if (hasDescription) { 135 activeStyle = descriptionId.ToLower() == firstGroupID.ToLower() ? "active" : ""; 136 137 <li class="nav-item"> 138 <a 139 class="nav-link @descriptionId @activeStyle" 140 id="@descriptionId-tab" 141 data-bs-toggle="tab" 142 href="#@descriptionId" 143 role="tab" 144 aria-controls="@descriptionId" 145 aria-selected="true" 146 > 147 <span class="">@Translate("Description")</span> 148 </a> 149 </li> 150 } 151 152 @if (hasDocuments) { 153 <li class="nav-item"> 154 <a 155 class="nav-link" 156 id="Downloads-tab" 157 data-bs-toggle="tab" 158 href="#Downloads" 159 role="tab" 160 aria-controls="Downloads" 161 aria-selected="true" 162 > 163 <span class="">@Translate("Downloads")</span> 164 </a> 165 </li> 166 } 167 </ul> 168 <div id="productTabContent" class="tab-content g-col-12 pt-2"> 169 @foreach (var group in displayGroups) 170 { 171 if (group.SystemName.ToLower() == firstGroupID.ToLower()) 172 { 173 activeStyle = "show active"; 174 } 175 else 176 { 177 activeStyle = ""; 178 } 179 180 <div id="@group.SystemName" 181 class="tab-pane fade @activeStyle" 182 role="tabpanel" 183 aria-labelledby="@group.SystemName-tab"> 184 @{ 185 templateParams["Groups"] = new List<FieldGroupViewModel> { group }; 186 @RenderPartial(templateLayoutPath, Model, templateParams); 187 } 188 </div> 189 } 190 191 @if (hasDescription) { 192 activeStyle = descriptionId.ToLower() == firstGroupID.ToLower() ? "show active" : ""; 193 194 <div 195 id="@descriptionId" 196 style="background-color: #f2f2f2;" 197 class="tab-pane fade @activeStyle p-3" 198 role="tabpanel" 199 aria-labelledby="@descriptionId-tab"> 200 <div> 201 @description 202 </div> 203 </div> 204 } 205 206 @if (hasDocuments) { 207 <div id="Downloads" 208 class="tab-pane fade" 209 role="tabpanel" 210 aria-labelledby="Downloads-tab"> 211 <div class="table-responsive"> 212 <table class="table table-hover align-middle mb-0" style="table-layout: fixed;"> 213 <thead> 214 <tr> 215 @* <th style="width:60px">&nbsp;</th> *@ 216 <th>@Translate("Name")</th> 217 <th class="text-end d-none d-lg-table-cell">@Translate("Download")</th> 218 <th class="text-end" style="width:100px">@Translate("File type")</th> 219 </tr> 220 </thead> 221 <tbody class="border-top-0"> 222 @foreach (MediaViewModel asset in documentAssets) 223 { 224 string assetValue = asset.Value; 225 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Value.Substring(asset.Value.LastIndexOf('/') + 1); 226 227 if (assetValue.StartsWith("/Files/", StringComparison.OrdinalIgnoreCase)) 228 { 229 string filePath = Dynamicweb.Context.Current.Server.MapPath(assetValue); 230 231 if (File.Exists(filePath)) 232 { 233 long fileSize = new FileInfo(filePath).Length / 1024; 234 235 foreach (string format in supportedDocumentFormats) 236 { 237 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 238 { 239 string productName = product.Name; 240 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 241 string thumbnailPath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage?.Value; 242 string thumbnailLinkPath = thumbnailPath; 243 thumbnailPath = $"/Admin/Public/GetImage.ashx?image={thumbnailPath}&width=60&format=webp"; 244 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : ""; 245 246 RatioSettings ratioSettings = GetRatioSettings(); 247 248 <tr> 249 @* <td class="px-0"> 250 <a href="@thumbnailLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@productName"> 251 @if (assetValue.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0) 252 { 253 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 254 <img loading="lazy" src="@thumbnailPath" class="mw-100 mh-100" alt="@productName" @assetTitle /> 255 </div> 256 } 257 else 258 { 259 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100"> 260 <div class="icon-3 position-absolute" style="z-index: 1">@ReadFile(iconPath + "file-text.svg")</div> 261 </div> 262 } 263 </a> 264 </td> *@ 265 <td> 266 <a href="@assetValue" class="text-decoration-none text-break" download="@assetName" title="@assetName"> 267 @assetName 268 </a> 269 </td> 270 <td class="text-end d-none d-lg-table-cell"> 271 <a href="@assetValue" class="text-decoration-none" download="@assetName" title="@assetName"> 272 @fileSize KB <div class="icon-2 d-inline-block" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 273 </a> 274 </td> 275 <td class="text-end">@format</td> 276 </tr> 277 } 278 } 279 } 280 } 281 else 282 { 283 <tr> 284 <td class="px-0">&nbsp;</td> 285 <td> 286 <a href="@assetValue" class="text-decoration-none text-break" target="_blank" title="@assetName"> 287 @assetName 288 </a> 289 </td> 290 <td class="text-end d-none d-lg-table-cell">&nbsp;</td> 291 <td class="text-end">&nbsp;</td> 292 </tr> 293 } 294 } 295 </tbody> 296 </table> 297 </div> 298 </div> 299 } 300 </div> 301 </div> 302 </div> 303 304 } 305 else 306 { 307 <div class="alert alert-warning m-0">@Translate("Product Specification will be shown here if any")</div> 308 } 309 } 310
By clicking 'Accept All' you consent that we may collect information about you for various purposes, including: Statistics and Marketing