Error executing template "Designs/Swift-v2/Paragraph/Swift-v2_ProductMedia.cshtml" System.NullReferenceException: Object reference not set to an instance of an object. at CompiledRazorTemplates.Dynamic.RazorEngine_aea9e03f10e44302a2982b3d35d93c9d.<ExecuteAsync>b__0_3(MediaViewModel asset) at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source, Func`2 predicate) at CompiledRazorTemplates.Dynamic.RazorEngine_aea9e03f10e44302a2982b3d35d93c9d.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 string GetVideoType(string assetValue) 70 { 71 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : string.Empty; 72 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type; 73 type = string.IsNullOrEmpty(type) ? "selfhosted" : type; 74 75 return type; 76 } 77 78 public string GetYoutubeScreenDump(string assetValue, string quality) 79 { 80 var regex = new Regex(@"(?:youtube\.com\/.*[\?&]v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]+)(?:\?.*)?"); 81 Match match = regex.Match(assetValue); 82 string videoId = match.Success ? match.Groups[1].Value : string.Empty; 83 string youtubeThumbnail = $"https://img.youtube.com/vi/{videoId}/{quality}.jpg"; 84 return youtubeThumbnail; 85 } 86 87 public string GetFileTypeIcon(string filePath) 88 { 89 string fileType = Path.GetExtension(filePath).ToLower(); 90 string fileTypeIconPath = "/Files/Templates/Designs/Swift-v2/Assets/images/FileTypes/"; 91 var iconMap = new Dictionary<string, string> 92 { 93 { ".pdf", $"{fileTypeIconPath}pdf.svg" }, 94 { ".docx", $"{fileTypeIconPath}docx.svg" }, 95 { ".xlsx", $"{fileTypeIconPath}xlsx.svg" }, 96 { ".ppt", $"{fileTypeIconPath}ppt.svg" } 97 }; 98 return iconMap.ContainsKey(fileType) ? iconMap[fileType] : $"/Files/Images/Icons/download.svg"; 99 } 100 101 public bool MatchesFormat(string assetValue, string[] formats) 102 { 103 return formats.Any(format => assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0); 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 } 114 115 @if (product is object) 116 { 117 @* Supported formats *@ 118 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" }; 119 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" }; 120 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" }; 121 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray(); 122 123 @* Collect the assets *@ 124 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue().OfType<string>(); 125 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages"); 126 127 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@ 128 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : ""; 129 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets); 130 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage)); 131 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { }; 132 assetsList = assetsList.Union(assetsImages); 133 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages) : assetsList; 134 assetsList = includeImagePatternImages && assetsList.Count() == 0 ? assetsList.Append(product.DefaultImage) : assetsList; 135 136 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback"); 137 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage"); 138 139 int totalAssets = 0; 140 if (showOnlyPrimaryImage == false) 141 { 142 totalAssets = assetsList.Count(asset => MatchesFormat(asset.Value, allSupportedFormats)); 143 } 144 145 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count() == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback) 146 { 147 assetsList = new List<MediaViewModel>() { product.DefaultImage }; 148 totalAssets = 1; 149 } 150 151 @* Get assets from selected categories or get all assets *@ 152 if (totalAssets != 0) 153 { 154 int assetNumber = 0; 155 int thumbnailNumber = 0; 156 int modalAssetNumber = 0; 157 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty; 158 159 <div class="d-flex gap-3 h-100 @(thumbnailAxisCss) item_@Model.Item.SystemName.ToLower()" data-dw-colorscheme="@Model.ColorScheme?.Id"> 160 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel"> 161 <div class="carousel-inner h-100"> 162 @foreach (MediaViewModel asset in assetsList) 163 { 164 var assetValue = asset.Value; 165 foreach (string format in allSupportedFormats) 166 { 167 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 168 { 169 string activeSlide = assetNumber == 0 ? "active" : ""; 170 171 <div class="carousel-item @activeSlide" data-bs-interval="99999"> 172 @{ 173 string size = "mobile"; 174 175 <div class="h-100"> 176 @foreach (string imageFormat in supportedImageFormats) 177 { //Images 178 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 179 { 180 if (product is object) 181 { 182 string productName = product.Name; 183 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 184 185 RatioSettings ratioSettings = GetRatioSettings(size); 186 187 var parms = new Dictionary<string, object>(); 188 parms.Add("alt", productName + asset.Keywords); 189 parms.Add("itemprop", "image"); 190 parms.Add("columns", Model.GridRowColumnCount); 191 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading")); 192 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage")); 193 if (!string.IsNullOrEmpty(asset.DisplayName)) 194 { 195 parms.Add("title", asset.DisplayName); 196 } 197 198 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 199 { 200 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 201 } 202 else 203 { 204 parms.Add("cssClass", "mw-100 mh-100"); 205 } 206 207 <a href="@imagePath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 208 <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"> 209 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 210 </div> 211 </a> 212 } 213 } 214 } 215 @foreach (string videoFormat in supportedVideoFormats) 216 { //Videos 217 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 218 { 219 if (product is object) { 220 var video = asset.GetVideoViewModel(); 221 222 if (Model.Item.GetString("OpenVideoInModal") == "true") 223 { 224 string iconPath = "/Files/Images/Icons/"; 225 226 string productName = product.Name; 227 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 228 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : ""; 229 230 RatioSettings ratioSettings = GetRatioSettings(size); 231 232 string type = GetVideoType(asset.Value); 233 234 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "maxresdefault") : string.Empty; 235 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : ""; 236 237 238 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 239 <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"> 240 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 241 @if (video.IsExternalLink()) 242 { 243 <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;"> 244 } 245 else 246 { 247 248 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 249 <source src="@(asset.Value)#t=0.001" type="@video.GetVideoType()"> 250 </video> 251 } 252 </div> 253 </div> 254 } 255 else 256 { 257 @RenderPartial("Components/VideoPlayer.cshtml", video) 258 } 259 } 260 } 261 } 262 @foreach (string documentFormat in supportedDocumentFormats) 263 { //Documents 264 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 265 { 266 if (product is object) 267 { 268 string productName = product.Name; 269 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 270 string fileTypeIcon = GetFileTypeIcon(asset.Value); 271 272 RatioSettings ratioSettings = GetRatioSettings(size); 273 274 var parms = new Dictionary<string, object>(); 275 parms.Add("alt", productName + asset.Keywords); 276 parms.Add("itemprop", "image"); 277 parms.Add("fullwidth", true); 278 parms.Add("columns", Model.GridRowColumnCount); 279 280 if (!string.IsNullOrEmpty(asset.DisplayName)) 281 { 282 parms.Add("title", asset.DisplayName); 283 } 284 285 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 286 { 287 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 288 } 289 else 290 { 291 parms.Add("cssClass", "mw-100 mh-100"); 292 } 293 294 <a href="@imagePath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download title="@Translate("Download"): @(asset.Name)@Path.GetExtension(asset.Value).ToLower()"> 295 <div class="d-flex align-items-center justify-content-center text-center overflow-hidden h-100 border"> 296 <div class="icon-5 position-absolute" style="z-index: 1"> 297 @ReadFile(fileTypeIcon) 298 </div> 299 </div> 300 </a> 301 } 302 303 } 304 } 305 </div> 306 } 307 308 309 </div> 310 assetNumber++; 311 } 312 } 313 } 314 </div> 315 316 </div> 317 318 @if (totalAssets > 1) 319 { 320 <div class="@(GetThumbnailRowSettingCss()) gap-3" id="SmallScreenImagesThumbnails_@Model.ID"> 321 @foreach (MediaViewModel asset in assetsList) 322 { 323 var assetValue = asset.Value; 324 string assetName = asset.Name; 325 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 326 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : null; 327 string iconPath = "/Files/Images/Icons/"; 328 329 string imagePath = assetValue; 330 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; 331 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; 332 333 RatioSettings ratioSettings = GetRatioSettings("desktop"); 334 335 <div class="border outline-none position-relative flex-grow-0 flex-shrink-0 @(ratioSettings.CssClass)" style="@(ratioSettings.CssVariable); cursor: pointer; width: clamp(4.5rem, 18vw, 8rem);" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber"> 336 @foreach (string imageFormat in supportedImageFormats) 337 { //Images 338 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 339 { 340 <img src="@imagePathThumb" alt="@assetName" class="p-0 p-lg-1 w-100 h-100" style="object-fit: contain;"> 341 342 thumbnailNumber++; 343 } 344 } 345 346 @foreach (string videoFormat in supportedVideoFormats) 347 { //Videos 348 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 349 { 350 var video = asset.GetVideoViewModel(); 351 string type = GetVideoType(asset.Value); 352 353 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "mqdefault") : ""; 354 videoScreendumpPath = type == "vimeo" ? string.Empty : videoScreendumpPath; 355 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : string.Empty; 356 357 <div class="icon-5 position-absolute top-50 start-50 translate-middle" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 358 359 if (video.IsExternalLink()) 360 { 361 <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;" /> 362 } 363 else 364 { 365 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 366 <source src="@(asset.Value)#t=0.001" type="@video.GetVideoType()"> 367 </video> 368 } 369 370 thumbnailNumber++; 371 } 372 } 373 374 @foreach (string documentFormat in supportedDocumentFormats) 375 { //Documents 376 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 377 { 378 string fileTypeIcon = GetFileTypeIcon(asset.Value); 379 380 <a href="@assetValue" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 4rem; max-width: 8rem;" download title="@Translate("Download"): @(asset.Name)@Path.GetExtension(asset.Value).ToLower()"> 381 <div class="d-flex align-items-center justify-content-center text-center overflow-hidden h-100 border"> 382 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(fileTypeIcon)</div> 383 </div> 384 </a> 385 386 thumbnailNumber++; 387 } 388 } 389 </div> 390 } 391 </div> 392 } 393 </div> 394 395 @* Modal with slides *@ 396 <div class="modal fade" id="modal_@Model.ID" tabindex="-1" aria-labelledby="mediaModalTitle_@Model.ID" aria-hidden="true"> 397 <div class="modal-dialog modal-dialog-centered modal-xl"> 398 <div class="modal-content"> 399 <div class="modal-header visually-hidden"> 400 <h5 class="modal-title" id="mediaModalTitle_@Model.ID">@product.Title</h5> 401 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 402 </div> 403 <div class="modal-body p-2 p-lg-3 h-100"> 404 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel"> 405 <div class="carousel-inner h-100" data-dw-colorscheme="@Model.ColorScheme?.Id"> 406 @foreach (MediaViewModel asset in assetsList) 407 { 408 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 409 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray()) 410 { 411 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0) 412 { 413 string imagePath = assetValue; 414 string activeSlide = modalAssetNumber == 0 ? "active" : ""; 415 416 var parms = new Dictionary<string, object>(); 417 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto"); 418 parms.Add("fullwidth", true); 419 parms.Add("columns", Model.GridRowColumnCount); 420 421 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999"> 422 @foreach (string imageFormat in supportedImageFormats) 423 { //Images 424 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 425 { 426 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 427 } 428 } 429 430 @foreach (string videoFormat in supportedVideoFormats) 431 { //Videos 432 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 433 { 434 @RenderPartial("Components/VideoPlayer.cshtml", asset.GetVideoViewModel()) 435 } 436 } 437 </div> 438 modalAssetNumber++; 439 } 440 } 441 } 442 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev"> 443 <span class="carousel-control-prev-icon" aria-hidden="true"></span> 444 <span class="visually-hidden">@Translate("Previous")</span> 445 </button> 446 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next"> 447 <span class="carousel-control-next-icon" aria-hidden="true"></span> 448 <span class="visually-hidden">@Translate("Next")</span> 449 </button> 450 </div> 451 </div> 452 </div> 453 </div> 454 </div> 455 </div> 456 } 457 else if (Pageview.IsVisualEditorMode) 458 { 459 RatioSettings ratioSettings = GetRatioSettings("desktop"); 460 461 <div class="h-100" data-dw-colorscheme="@Model.ColorScheme?.Id"> 462 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)"> 463 <img src="/Files/Images/nopic.png" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;"> 464 </div> 465 </div> 466 } 467 } 468 else if (Pageview.IsVisualEditorMode) 469 { 470 <div class="alert alert-dark m-0">@Translate("The images will be shown here, if any")</div> 471 } 472 473 474 475
Shimano SH-R075 Road Shoe
€ 59,99
excl. VAT
In stock
- Form: Slim
- Gender: Men
- Brand name: Shimano
- Color:
- Composition: Nylon Leather
- Form: Slim
- Gender: Men
- Washing instruction: Do not wash
- Volume: 0 m³
- Weight: 0 kg
- Width: 0 cm
- Height: 0 cm
- Composition: Nylon Leather
- Form: Slim
- Gender: Men
- Washing instruction: Do not wash
- Form: Slim
- Gender: Men
Similar products