Dock Cleat Mounting Hardware (For 4 Cleats)

D1CSH
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_32944c1cb77b46fe8ba3b226424105f1.<ExecuteAsync>b__0_3(MediaViewModel asset)
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source, Func`2 predicate)
   at CompiledRazorTemplates.Dynamic.RazorEngine_32944c1cb77b46fe8ba3b226424105f1.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

Dock Cleat Mounting Hardware (For 4 Cleats)

D1CSH
29 In stock

Phone
Toll-Free (800) 544-2067
(507) 225-3421

Shipping Address
709 Old Hwy 14
Nicollet, MN 56074

Bolts And Nuts For Mounting Cleats, Enough For Four Tip Over Cleat Seats

Documents

Package contents

Your account

Sign in

Shopping cart