1<!DOCTYPE html> 2<!--[if IE 7]> 3<html class="ie ie7" lang="en-US"> 4<![endif]--> 5<!--[if IE 8]> 6<html class="ie ie8" lang="en-US"> 7<![endif]--> 8<!--[if !(IE 7) & !(IE 8)]><!--> 9<html lang="en-US"> 10<!--<![endif]--> 11<head> 12<meta charset="UTF-8" /> 13<meta name="viewport" content="width=device-width" /> 14<title>Fritz on the Web | My little corner of the web, full of my point of view</title> 15<link rel="profile" href="https://gmpg.org/xfn/11" /> 16<link rel="pingback" href="https://jeffreyfritz.com/xmlrpc.php"> 17<!--[if lt IE 9]>
18<script src="https://jeffreyfritz.com/wp-content/themes/twentytwelve/js/html5.js?ver=3.7.0" type="text/javascript"></script>
18 19<![endif]--> 20<meta name='robots' content='max-image-preview:large' /> 21<link rel='dns-prefetch' href='//fonts.googleapis.com' /> 22<link href='https://fonts.gstatic.com' crossorigin rel='preconnect' /> 23<link rel="alternate" type="application/rss+xml" title="Fritz on the Web » Feed" href="https://jeffreyfritz.com/feed/" /> 24<link rel="alternate" type="application/rss+xml" title="Fritz on the Web » Comments Feed" href="https://jeffreyfritz.com/comments/feed/" />
25<script type="text/javascript"> 26/* <![CDATA[ */ 27window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/15.0.3\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/jeffreyfritz.com\/wp-includes\/js\/wp-emoji-release.min.js?ver=6.5.12"}}; 28/*! This file is auto-generated */ 29!function(i,n){var o,s,e;function c(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function p(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data),r=(e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0),new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data));return t.every(function(e,t){return e===r[t]})}function u(e,t,n){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\uddfa\ud83c\uddf3","\ud83c\uddfa\u200b\ud83c\uddf3")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!n(e,"\ud83d\udc26\u200d\u2b1b","\ud83d\udc26\u200b\u2b1b")}return!1}function f(e,t,n){var r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):i.createElement("canvas"),a=r.getContext("2d",{willReadFrequently:!0}),o=(a.textBaseline="top",a.font="600 32px Arial",{});return e.forEach(function(e){o[e]=t(a,e,n)}),o}function t(e){var t=i.createElement("script");t.src=e,t.defer=!0,i.head.appendChild(t)}"undefined"!=typeof Promise&&(o="wpEmojiSettingsSupports",s=["flag","emoji"],n.supports={everything:!0,everythingExceptFlag:!0},e=new Promise(function(e){i.addEventListener("DOMContentLoaded",e,{once:!0})}),new Promise(function(t){var n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),p.toString()].join(",")+"));",r=new Blob([e],{type:"text/javascript"}),a=new Worker(URL.createObjectURL(r),{name:"wpTestEmojiSupports"});return void(a.onmessage=function(e){c(n=e.data),a.terminate(),t(n)})}catch(e){}c(n=f(s,u,p))}t(n)}).then(function(e){for(var t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=function(){n.DOMReady=!0}}).then(function(){return e}).then(function(){var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}))}((window,document),window._wpemojiSettings); 30/* ]]> */ 31</script>
31 32<style id='wp-emoji-styles-inline-css' type='text/css'> 33 34 img.wp-smiley, img.emoji { 35 display: inline !important; 36 border: none !important; 37 box-shadow: none !important; 38 height: 1em !important; 39 width: 1em !important; 40 margin: 0 0.07em !important; 41 vertical-align: -0.1em !important; 42 background: none !important; 43 padding: 0 !important; 44 } 45</style> 46<link rel='stylesheet' id='wp-block-library-css' href='https://jeffreyfritz.com/wp-includes/css/dist/block-library/style.min.css?ver=6.5.12' type='text/css' media='all' /> 47<style id='wp-block-library-theme-inline-css' type='text/css'> 48.wp-block-audio figcaption{color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-audio figcaption{color:#ffffffa6}.wp-block-audio{margin:0 0 1em}.wp-block-code{border:1px solid #ccc;border-radius:4px;font-family:Menlo,Consolas,monaco,monospace;padding:.8em 1em}.wp-block-embed figcaption{color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-embed figcaption{color:#ffffffa6}.wp-block-embed{margin:0 0 1em}.blocks-gallery-caption{color:#555;font-size:13px;text-align:center}.is-dark-theme .blocks-gallery-caption{color:#ffffffa6}.wp-block-image figcaption{color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-image figcaption{color:#ffffffa6}.wp-block-image{margin:0 0 1em}.wp-block-pullquote{border-bottom:4px solid;border-top:4px solid;color:currentColor;margin-bottom:1.75em}.wp-block-pullquote cite,.wp-block-pullquote footer,.wp-block-pullquote__citation{color:currentColor;font-size:.8125em;font-style:normal;text-transform:uppercase}.wp-block-quote{border-left:.25em solid;margin:0 0 1.75em;padding-left:1em}.wp-block-quote cite,.wp-block-quote footer{color:currentColor;font-size:.8125em;font-style:normal;position:relative}.wp-block-quote.has-text-align-right{border-left:none;border-right:.25em solid;padding-left:0;padding-right:1em}.wp-block-quote.has-text-align-center{border:none;padding-left:0}.wp-block-quote.is-large,.wp-block-quote.is-style-large,.wp-block-quote.is-style-plain{border:none}.wp-block-search .wp-block-search__label{font-weight:700}.wp-block-search__button{border:1px solid #ccc;padding:.375em .625em}:where(.wp-block-group.has-background){padding:1.25em 2.375em}.wp-block-separator.has-css-opacity{opacity:.4}.wp-block-separator{border:none;border-bottom:2px solid;margin-left:auto;margin-right:auto}.wp-block-separator.has-alpha-channel-opacity{opacity:1}.wp-block-separator:not(.is-style-wide):not(.is-style-dots){width:100px}.wp-block-separator.has-background:not(.is-style-dots){border-bottom:none;height:1px}.wp-block-separator.has-background:not(.is-style-wide):not(.is-style-dots){height:2px}.wp-block-table{margin:0 0 1em}.wp-block-table td,.wp-block-table th{word-break:normal}.wp-block-table figcaption{color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-table figcaption{color:#ffffffa6}.wp-block-video figcaption{color:#555;font-size:13px;text-align:center}.is-dark-theme .wp-block-video figcaption{color:#ffffffa6}.wp-block-video{margin:0 0 1em}.wp-block-template-part.has-background{margin-bottom:0;margin-top:0;padding:1.25em 2.375em} 49</style> 50<style id='kevinbatdorf-code-block-pro-style-inline-css' type='text/css'> 51.wp-block-kevinbatdorf-code-block-pro{direction:ltr!important;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;-webkit-text-size-adjust:100%!important;box-sizing:border-box!important;position:relative!important}.wp-block-kevinbatdorf-code-block-pro *{box-sizing:border-box!important}.wp-block-kevinbatdorf-code-block-pro pre,.wp-block-kevinbatdorf-code-block-pro pre *{font-size:inherit!important;line-height:inherit!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor) pre{background-image:none!important;border:0!important;border-radius:0!important;border-style:none!important;border-width:0!important;color:inherit!important;font-family:inherit!important;margin:0!important;overflow:auto!important;overflow-wrap:normal!important;padding:16px 0 16px 16px!important;text-align:left!important;white-space:pre!important;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-sha
51dow)!important}.wp-block-kevinbatdorf-code-block-pro.padding-disabled:not(.code-block-pro-editor) pre{padding:0!important}.wp-block-kevinbatdorf-code-block-pro.padding-bottom-disabled pre{padding-bottom:0!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor) pre code{background:none!important;background-color:transparent!important;border:0!important;border-radius:0!important;border-style:none!important;border-width:0!important;color:inherit!important;display:block!important;font-family:inherit!important;margin:0!important;overflow-wrap:normal!important;padding:0!important;text-align:left!important;white-space:pre!important;width:100%!important;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor) pre code .line{display:inline-block!important;min-width:var(--cbp-block-width,100%)!important;vertical-align:top!important}.wp-block-kevinbatdorf-code-block-pro.cbp-has-line-numbers:not(.code-block-pro-editor) pre code .line{padding-left:calc(12px + var(--cbp-line-number-width, auto))!important}.wp-block-kevinbatdorf-code-block-pro.cbp-has-line-numbers:not(.code-block-pro-editor) pre code{counter-increment:step calc(var(--cbp-line-number-start, 1) - 1)!important;counter-reset:step!important}.wp-block-kevinbatdorf-code-block-pro pre code .line{position:relative!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor) pre code .line:before{content:""!important;display:inline-block!important}.wp-block-kevinbatdorf-code-block-pro.cbp-has-line-numbers:not(.code-block-pro-editor) pre code .line:not(.cbp-line-number-disabled):before{color:var(--cbp-line-number-color,#999)!important;content:counter(step)!important;counter-increment:step!important;left:0!important;opacity:.5!important;position:absolute!important;text-align:right!important;transition-duration:.5s!important;transition-property:opacity!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important;width:var(--cbp-line-number-width,auto)!important}.wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover .line{min-height:var(--cbp-block-height,100%)!important}.wp-block-kevinbatdorf-code-block-pro .line.cbp-line-highlight .cbp-line-highlighter,.wp-block-kevinbatdorf-code-block-pro .line.cbp-no-blur:hover .cbp-line-highlighter,.wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover:not(.cbp-blur-enabled:not(.cbp-unblur-on-hover)) .line:hover .cbp-line-highlighter{background:var(--cbp-line-highlight-color,rgb(14 165 233/.2))!important;left:-16px!important;min-height:var(--cbp-block-height,100%)!important;min-width:calc(var(--cbp-block-width, 100%) + 16px)!important;pointer-events:none!important;position:absolute!important;top:0!important;width:100%!important}[data-code-block-pro-font-family="Code-Pro-Comic-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro .line.cbp-line-highlight .cbp-line-highlighter,[data-code-block-pro-font-family="Code-Pro-Comic-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro .line.cbp-no-blur:hover .cbp-line-highlighter,[data-code-block-pro-font-family="Code-Pro-Comic-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover:not(.cbp-blur-enabled:not(.cbp-unblur-on-hover)) .line:hover .cbp-line-highlighter{top:-.125rem!important}[data-code-block-pro-font-family=Code-Pro-Fira-Code].wp-block-kevinbatdorf-code-block-pro .line.cbp-line-highlight .cbp-line-highlighter,[data-code-block-pro-font-family=Code-Pro-Fira-Code].wp-block-kevinbatdorf-code-block-pro .line.cbp-no-blur:hover .cbp-line-highlighter,[data-code-block-pro-font-family=Code-Pro-Fira-Code].wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover:not(.cbp-blur-enabled:not(.cbp-unblur-on-hover)) .line:hover .cbp-line-highlighter{top:-1.5px!important}[data-code-block-pro-font-family="Code-Pro-Deja-Vu-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro .line.cbp-line-highlight .cbp-line-highlighter,[data-code-block-pro-font-family="Code-Pro-Deja-Vu-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro .line.cbp-no-blur:hover .cbp-line-highlighter,[data-code-block-pro-font-family="Code-Pro-Deja-Vu-Mono.ttf"].wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover:not(.cbp-blur-enabled:not(.cbp-unblur-on-hover)) .line:hover .cbp-line-highlighter,[data-code-block-pro-font-family=Code-Pro-Cozette].wp-block-kevinbatdorf-code-block-pro .line.cbp-line-highlight .cbp-line-highlighter,[data-code-block-pro-font-family=Code-Pro-Cozette].wp-block-kevinbatdorf-code-block-pro .line.cbp-no-blur:hover .cbp-line-highlighter,[data-code-block-pro-font-family=Code-Pro-Cozette].wp-block-kevinbatdorf-code-block-pro.cbp-highlight-hover:not(.cbp-blur-enabled:not(.cbp-unblur-on-hover)) .line:hover .cbp-line-highlighter{top:-1px!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor).padding-disabled pre .line.cbp-line-highlight:after{left:0!important;width:100%!important}.wp-block-kevinbatdorf-code-block-pro.cbp-blur-enabled pre .line:not(.cbp-no-blur){filter:blur(1px)!important;opacity:.4!important;pointer-events:none!important;transition-duration:.2s!important;transition-property:all!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important}.wp-block-kevinbatdorf-code-block-pro.cbp-blur-enabled.cbp-unblur-on-hover:hover pre .line:not(.cbp-no-blur){opacity:1!important;pointer-events:auto!important;--tw-blur:blur(0);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.wp-block-kevinbatdorf-code-block-pro:not(.code-block-pro-editor) pre *{font-family:inherit!important}.cbp-see-more-simple-btn-hover{transition-property:none!important}.cbp-see-more-simple-btn-hover:hover{box-shadow:inset 0 0 100px 100px hsla(0,0%,100%,.1)!important}.code-block-pro-copy-button{border:0!important;border-style:none!important;border-width:0!important;cursor:pointer!important;left:auto!important;line-height:1!important;opacity:.1!important;padding:6px!important;position:absolute!important;right:0!important;top:0!important;transition-duration:.2s!important;transition-property:opacity!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;z-index:10!important}.code-block-pro-copy-button:focus{opacity:.4!important}.code-block-pro-copy-button:not([data-has-text-button]){background:none!important;background-color:transparent!important}.wp-block-kevinbatdorf-code-block-pro.padding-disabled .code-block-pro-copy-button{padding:0!important}.wp-block-kevinbatdorf-code-block-pro:hover .code-block-pro-copy-button{opacity:.5!important}.wp-block-kevinbatdorf-code-block-pro .code-block-pro-copy-button:hover{opacity:.9!important}.code-block-pro-copy-button[data-has-text-button],.wp-block-kevinbatdorf-code-block-pro:hover .code-block-pro-copy-button[data-has-text-button]{opacity:1!important}.wp-block-kevinbatdorf-code-block-pro .code-block-pro-copy-button[data-has-text-button]:hover{opacity:.8!important}.code-block-pro-copy-button[data-has-text-button]{border-radius:.75rem!important;display:block!important;margin-right:.75rem!important;margin-top:.7rem!important;padding:.125rem .375rem!important}.code-block-pro-copy-button[data-inside-header-type=headlightsMuted],.code-block-pro-copy-button[data-inside-header-type^=headlights]{margin-top:.85rem!important}.code-block-pro-copy-button[data-inside-header-type=headlightsMutedAlt]{margin-top:.65rem!important}.code-block-pro-copy-button[data-inside-header-type=simpleString]{margin-top:.645rem!important}.code-block-pro-copy-button[data-inside-header-type=pillString]{margin-top:1rem!important}.code-block-pro-copy-button[data-inside-header-type=pillString] .cbp-btn-text{position:relative!important;top:1px!important}.cbp-btn-text{font-size:.75rem!important;line-height:1rem!important}.code-block-pro-copy-button .without-check{display:block!important}.code-block-pro-copy-button .with-check{display:none!important}.code-block-pro-copy-button.cbp-copying{opacity:1!important}.code-block-pro-copy-button.cbp-copying .without-check{display:none!important}.code-block-pro-copy-button.cbp-copying .with-check{display:block!important}.cbp-footer-link:hover{text-decoration-line:underline!important}@media print{.wp-block-kevinbatdorf-code-block-pro pre{max-height:none!important}.wp-block-kevinbatdorf-code-block-pro:not(#x) .line:before{background-color:transparent!important;color:inherit!important}.wp-block-kevinbatdorf-code-block-pro:not(#x) .cbp-line-highlighter,.wp-block-kevinbatdorf-code-block-pro:not(#x)>span{display:none!important}} 52 53</style> 54<style id='classic-theme-styles-inline-css' type='text/css'> 55/*! This file is auto-generated */ 56.wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;
56padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none} 57</style> 58<style id='global-styles-inline-css' type='text/css'> 59body{--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #fff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--color--blue: #21759b;--wp--preset--color--dark-gray: #444;--wp--preset--color--medium-gray: #9f9f9f;--wp--preset--color--light-gray: #e6e6e6;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);
59--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}
59.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;} 60.wp-block-navigation a:where(:not(.wp-element-button)){color: inherit;} 61:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;} 62:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;} 63.wp-block-pullquote{font-size: 1.5em;line-height: 1.6;} 64</style> 65<link rel='stylesheet' id='twentytwelve-fonts-css' href='https://fonts.googleapis.com/css?family=Open+Sans%3A400italic%2C700italic%2C400%2C700&subset=latin%2Clatin-ext&display=fallback' type='text/css' media='all' /> 66<link rel='stylesheet' id='twentytwelve-style-css' href='https://jeffreyfritz.com/wp-content/themes/twentytwelve/style.css?ver=20221101' type='text/css' media='all' /> 67<link rel='stylesheet' id='twentytwelve-block-style-css' href='https://jeffreyfritz.com/wp-content/themes/twentytwelve/css/blocks.css?ver=20190406' type='text/css' media='all' /> 68<!--[if lt IE 9]> 69<link rel='stylesheet' id='twentytwelve-ie-css' href='https://jeffreyfritz.com/wp-content/themes/twentytwelve/css/ie.css?ver=20150214' type='text/css' media='all' /> 70<![endif]-->
71<script type="text/javascript" src="https://jeffreyfritz.com/wp-includes/js/jquery/jquery.min.js?ver=3.7.1" id="jquery-core-js"></script>
vendor: 1 bytes, line 71
71
72<script type="text/javascript" src="https://jeffreyfritz.com/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1" id="jquery-migrate-js"></script>
72 73<link rel="https://api.w.org/" href="https://jeffreyfritz.com/wp-json/" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://jeffreyfritz.com/xmlrpc.php?rsd" /> 74<meta name="generator" content="WordPress 6.5.12" /> 75</head> 76 77<body class="home blog wp-embed-responsive custom-font-enabled single-author"> 78<div id="page" class="hfeed site"> 79 <header id="masthead" class="site-header"> 80 <hgroup> 81 <h1 class="site-title"><a href="https://jeffreyfritz.com/" title="Fritz on the Web" rel="home">Fritz on the Web</a></h1> 82 <h2 class="site-description">My little corner of the web, full of my point of view</h2> 83 </hgroup> 84 85 <a href="https://jeffreyfritz.com/"><img src="https://jeffreyfritz.com/wp-content/uploads/2015/11/cropped-cropped-header_compressed.jpg" class="header-image" width="1256" height="327" alt="Fritz on the Web" /></a> 86 87 <nav id="site-navigation" class="main-navigation"> 88 <button class="menu-toggle">Menu</button> 89 <a class="assistive-text" href="#content" title="Skip to content">Skip to content</a> 90 <div class="menu-menu-1-container"><ul id="menu-menu-1" class="nav-menu"><li id="menu-item-11605" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-11605"><a href="https://jeffreyfritz.com" aria-current="page">Home</a></li> 91<li id="menu-item-11609" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-11609"><a href="https://jeffreyfritz.com/live/">Live Stream</a></li> 92<li id="menu-item-11608" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-11608"><a href="https://jeffreyfritz.com/about/">About Jeff</a></li> 93<li id="menu-item-11655" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-11655"><a href="https://jeffreyfritz.com/thank-you-supporters/">Thank you supporters</a></li> 94</ul></div> </nav><!-- #site-navigation --> 95 96 97 </header><!-- #masthead --> 98 99 <div id="main" class="wrapper"> 100 101 <div id="primary" class="site-content"> 102 <div id="content" role="main"> 103 104 105 <article id="post-11659" class="post-11659 post type-post status-publish format-standard has-post-thumbnail hentry category-net category-asp-net tag-net-aspire tag-aspnet tag-docker"> 106 <header class="entry-header"> 107 <img width="624" height="337" src="https://jeffreyfritz.com/wp-content/uploads/2024/07/header-624x337.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt=".NET Aspire and Antivirus" decoding="async" fetchpriority="high" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/07/header-624x337.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/07/header-300x162.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/07/header-768x415.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/07/header.png 800w" sizes="(max-width: 624px) 100vw, 624px" /> 108 <h1 class="entry-title"> 109 <a href="https://jeffreyfritz.com/2024/07/adding-antivirus-to-net-aspire-systems/" rel="bookmark">Adding Antivirus to .NET Aspire Systems</a> 110 </h1> 111 <div class="comments-link"> 112 <a href="https://jeffreyfritz.com/2024/07/adding-antivirus-to-net-aspire-systems/#respond"><span class="dsq-postid" data-dsqidentifier="11659 https://jeffreyfritz.com/?p=11659"><span class="leave-reply">Leave a reply</span></span></a> </div><!-- .comments-link --> 113 </header><!-- .entry-header --> 114 115 <div class="entry-content"> 116 117<p>I was working on a web application over the weekend and I needed to add a feature that would allow images to be uploaded by end-users. As we all know, we should never trust content uploaded by anonymous ‘friends’ on the internet. I wanted to add malware scanning to my project, but how? In this article, I introduce a proof-of-concept project that adds the open source ClamAV antivirus scanner to a .NET Aspire system and shares connection information so that other resources can request malware scans.</p> 118 119 120 121<h2 class="wp-block-heading">The idea</h2> 122 123 124 125<p>I stumbled on a <a href="https://medium.com/@jeroenverhaeghe/building-a-malware-and-antivirus-scanner
125-in-net-core-e4309f2d429c" target="_blank" rel="noreferrer noopener nofollow">medium article by Jeroen Verhaeghe</a> where the <a href="https://www.clamav.net/" target="_blank" rel="noreferrer noopener nofollow">ClamAV antivirus scanner</a> was introduced in combination with the <a href="https://github.com/tekmaven/nClam" data-type="link" data-id="https://github.com/tekmaven/nClam" target="_blank" rel="noreferrer noopener nofollow">nClam library</a> from <a href="https://github.com/tekmaven" target="_blank" rel="noreferrer noopener nofollow">Ryan Hoffman</a>. Jeroen shows us how the ClamAV scanner can be run in a <a href="https://registry.hub.docker.com/r/clamav/clamav" target="_blank" rel="noreferrer noopener nofollow">Docker container</a>, and in reading that I thought – why not let .NET Aspire manage the container and its communications for us?</p> 126 127 128 129<h2 class="wp-block-heading">The Proof of Concept Code</h2> 130 131 132 133<p>ClamAV <a href="https://docs.clamav.net/manual/Installing/Docker.html" target="_blank" rel="noreferrer noopener nofollow">publishes a self-maintaining docker container</a> that updates itself with latest malware definitions. We know that containers can be added to a .NET Aspire project, so I wrote a quick resource that shared an HttpEndpoint:</p> 134 135 136
137<script src="https://gist.github.com/csharpfritz/a06d6a20b98b5ec259c49e5ac0f7ab49.js?file=clamav-resource.cs"></script>
137 138 139 140 141<p>Now we’re talking! We can now reference a ClamAV container resource easily in our .NET Aspire AppHost project and pass it to our website like this:</p> 142 143 144
145<script src="https://gist.github.com/csharpfritz/a06d6a20b98b5ec259c49e5ac0f7ab49.js?file=program.cs"></script>
145 146 147 148 149<figure class="wp-block-image size-large"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard.png"><img decoding="async" width="1024" height="335" src="https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard-1024x335.png" alt=".NET Aspire Dashboard showing the ClamAV antivirus resource" class="wp-image-11663" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard-1024x335.png 1024w, https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard-300x98.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard-768x252.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard-624x204.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/07/dashboard.png 1108w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure> 150 151 152 153<p>We can now scan our uploaded files with the nClam library like this:</p> 154 155 156
157<script src="https://gist.github.com/csharpfritz/a06d6a20b98b5ec259c49e5ac0f7ab49.js?file=service.cs"></script>
157 158 159 160 161<p>That’s pretty easy to get started with… and there’s a lot more that we can do with this to help make our public file uploads secure by default.</p> 162 163 164 165<h2 class="wp-block-heading">Get the Code!</h2> 166 167 168 169<p>I’ve published my sample code, showing a Blazor application uploading files to a minimal API endpoint where ClamAV inspects the content and reports back whether malware was detected. You can find it on my GitHub at <a href="https://github.com/csharpfritz/AspireAntivirus">https://github.com/csharpfritz/AspireAntivirus</a></p> 170 171 172 173<p>What do you think? Is this something that I should spend some more time wrapping and making easier to use? Create an issue on the GitHub repository if there’s something I can improve or a comment below to let me know what you think.</p> 174 </div><!-- .entry-content --> 175 176 <footer class="entry-meta"> 177 This entry was posted in <a href="https://jeffreyfritz.com/category/net/" rel="category tag">.NET</a>, <a href="https://jeffreyfritz.com/category/asp-net/" rel="category tag">ASP.NET</a> and tagged <a href="https://jeffreyfritz.com/tag/net-aspire/" rel="tag">.net aspire</a>, <a href="https://jeffreyfritz.com/tag/aspnet/" rel="tag">asp.net</a>, <a href="https://jeffreyfritz.com/tag/docker/" rel="tag">docker</a> on <a href="https://jeffreyfritz.com/2024/07/adding-antivirus-to-net-aspire-systems/" title="18:38" rel="bookmark"><time class="entry-date" datetime="2024-07-08T18:38:33-04:00">July 8, 2024</time></a><span class="by-author"> by <span class="author vcard"><a class="url fn n" href="https://jeffreyfritz.com/author/jfritz/" title="View all posts by Jeff" rel="author">Jeff</a></span></span>. </footer><!-- .entry-meta --> 178 </article><!-- #post --> 179 180 <article id="post-11637" class="post-11637 post type-post status-publish format-standard has-post-thumbnail hentry category-net category-net-maui category-uncategorized tag-net-maui tag-blazor"> 181 <header class="entry-header"> 182 <img width="624" height="351" src="https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI-624x351.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" decoding="async" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI-624x351.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI-300x169.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI-1024x576.png 1024w, https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI-768x432.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/04/NET-MAUI.png 1280w" sizes="(max-width: 624px) 100vw, 624px" /> 183 <h1 class="entry-title"> 184 <a href="https://jeffreyfritz.com/2024/04/seamless-navigation-in-net-maui-hybrid-apps-my-net-maui-hybrid-journey-part-1/" rel="bookmark">Seamless Navigation in .NET MAUI Hybrid Apps – My .NET MAUI Hybrid Journey (part 1)</a> 185 </h1> 186 </header><!-- .entry-header --> 187 188 <div class="entry-content"> 189 190<p>I’ve always wanted to build a native application with all the insight and expertise that I’ve accumulated in building web applications, it’s felt like a big jump for me to get into native mobile or desktop applications. A long time ago I used to build windows forms applications for my employer, and I haven’t worked a desktop application since.</p> 191 192 193 194<p>With the advent of <a href="https://dotnet.microsoft.com/en-us/apps/maui" target="_blank" rel="noreferrer noopener">.NET MAUI</a> and the ability to build Blazor applications for a website and share their pages and components for use in a native application felt like a real possibility for me. I haven’t really stepped into building and working working with this application model yet⦠until now.</p> 195 196 197 198<p>This is the first of what I expect will be a series of blog posts describing things that I’ve discovered, challenges that I’ve overcome, and features as I migrate the <a href="https://tagzapp.net" target="_blank" rel="noreferrer noopener">TagzApp application</a> to run as a native desktop application.</p> 199 200 201 202<h2 class="wp-block-heading">Menus – Not as Easy as They Look</h2> 203 204 205 206<p>The first feature that I wanted to bring over was going to be the layout of the application. In TagzApp I have a very simple layout with a header menu bar with a couple of options to allow you to navigate to other areas of the application. It’s not too complex but it felt like an easy piece to migrate over to the hybrid application.</p> 207 208 209 210<p>In doing some research and thinking more about how I want to represent a menubar inside of a native application it felt like it made more sense to turn this into a native menubar and not an HTML menubar that lived inside of my Blazor application. I started doing some research about how to create a menubar in .NET MAUI and found a few examples that showed how to use a tab bar to create and use multiple BlazorWebView components to represent different sections of the application. This felt clumsy to me because it meant that I would be spinning up multiple browsers to run inside of my application just to access and work with other parts of the application. I knew that that would mean more resources used by the computer when this application is running, and that felt a little irresponsible for me as a developer.</p> 211 212 213 214<p>I wanted to actually have a menu bar with items that you would click and it would navigate inside of my Blazor application. Looking at the documentation for the BlazorWebView, there is no direct access to the NavigationManager or an ability to reset the location of the browser component. I set about to make the NavigationManager inside of Blazor accessible to the .NET MAUI application.</p> 215 216 217 218<p>In the demos on this post, I’ll start with the default Blazor Hybrid template application and turn the vertical NavBar element into a
218native menu. <a href="https://github.com/csharpfritz/BlazorHybridNavigation" data-type="link" data-id="https://github.com/csharpfritz/BlazorHybridNavigation" target="_blank" rel="noreferrer noopener">Completed source code</a> for this sample is available on my GitHub. I also have recorded a video where I talk through this demo:</p> 219 220 221 222<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper"> 223<iframe loading="lazy" title="Blazor Navigation with .NET MAUI Components" width="625" height="352" src="https://www.youtube.com/embed/hmHOnRNl_j0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> 224</div></figure> 225 226 227<div class="wp-block-image"> 228<figure class="aligncenter size-full"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/04/image.png"><img loading="lazy" decoding="async" width="738" height="560" src="https://jeffreyfritz.com/wp-content/uploads/2024/04/image.png" alt="" class="wp-image-11638" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/04/image.png 738w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-300x228.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-624x473.png 624w" sizes="(max-width: 738px) 100vw, 738px" /></a><figcaption class="wp-element-caption">The default experience inside a Blazor Hybrid application with .NET 8</figcaption></figure></div> 229 230 231<h2 class="wp-block-heading">Configuring the Shell and Menu Items</h2> 232 233 234 235<p>To start with, I configured the App.xaml file to have a <a href="https://learn.microsoft.com/dotnet/maui/fundamentals/shell/" target="_blank" rel="noreferrer noopener">Shell</a> embedded directly and contain the BlazorWebView for my application. This would allow me to add a Menubar to the Shell.</p> 236 237 238 239<pre class="wp-block-code"><code><
239Application.MainPage> 240 <Shell> 241 <ShellContent> 242 <ShellContent.ContentTemplate> 243 <DataTemplate> 244 <ContentPage> 245 <BlazorWebView x:Name="blazorWebView1" HostPage="wwwroot/index.html"> 246 <BlazorWebView.RootComponents> 247 <RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" /> 248 </BlazorWebView.RootComponents> 249 </BlazorWebView> 250 </ContentPage> 251 </DataTemplate> 252 </ShellContent.ContentTemplate> 253 </ShellContent> 254 </Shell> 255</Application.MainPage> 256</code></pre> 257 258 259 260<p>I also removed the call inside App.xaml.cs to set <code>MainPage = new MainPage(); </code> Since we’re specifying our own MainPage inside the XAML markup, there’s no need to instantiate another page. I could run the application now, and I’d get the same user experience as the previous image.</p> 261 262 263 264<p>Ok.. next steps…</p> 265 266 267 268<h2 class="wp-block-heading">Adding a MenuBar component</h2> 269 270 271 272<p>In .NET MAUI, the MenuBar component is added when you introduce MenubarItems. No problem, I added a MenuBarItem and 3 MenuFlyoutItems for the 3 base pages inside the default application. This code was added just inside the <code>ContentPage</code> element in App.xaml</p> 273 274 275 276<pre class="wp-block-code"><code><ContentPage.MenuBarItems> 277 <MenuBarItem Text="Content"> 278 <MenuFlyoutItem Text="Home" Clicked="MenuItem_Clicked"></MenuFlyoutItem> 279 <MenuFlyoutItem Text="Counter" Clicked="MenuItem_Clicked"></MenuFlyoutItem> 280 <MenuFlyoutItem Text="Weather" Clicked="MenuItem_Clicked"></MenuFlyoutItem> 281 </MenuBarItem> 282</ContentPage.MenuBarItems></code></pre> 283 284 285 286<p>Notice that I set each of the menu items to trigger the same event, <code>MenuItem_Clicked</code> All of these menu items do the same thing, but vary in the location they target. We’ll write this method in a little bit, because we need to first make the NavigationManager available</p> 287 288 289 290<h2 class="wp-block-heading">Enabling the NavigationManager in .NET MAUI</h2> 291 292 293 294<p>The Blazor NavigationManager isn’t directly accessible in .NET MAUI. You can’t inject it or reach into the BlazorWebView and interact with it. Instead, we need to create a service that will allow us to capture the NavigationManager and interact with it. The curious part of this is that both parts of the application model, .NET MAUI and Blazor use the same dependency injection services. So…. we can exploit this to allow our service to be injected into both Blazor AND .NET MAUI.</p> 295 296 297 298<p>No problem, I can whip up a little bit of code that allows both application models to work with the Blazor NavigationManager:</p> 299 300 301 302<pre class="wp-block-code"><code>public class NavigatorService 303{ 304 305 internal NavigationManager NavigationManager { get; set; } 306 307} 308</code></pre> 309 310 311 312<p>I can then register this <code>NavigatorService</code> with the service locator in .NET MAUI with this line in the MauiProgram.cs file:</p> 313 314 315 316<pre class="wp-block-code"><code>builder.Services.AddSingleton<NavigatorService>();</code></pre> 317 318 319 320<p>I want this Navigator service on every page in my Blazor application, so I’ll inject it and configure the NavigationManager we’ll use inside the MainLayout.razor file:</p> 321 322 323 324<pre class="wp-block-code"><code>@inherits LayoutComponentBase 325@inject MauiApp1.NavigatorService NavigatorService 326@inject NavigationManager NavigationManager 327 328<div class="page"> 329... 330</div> 331 332@code { 333 334 protected override void OnInitialized() 335 { 336 337 NavigatorService.NavigationManager = NavigationManager; 338 339 } 340 341}</code></pre> 342 343 344 345<p>Finally, I’ll add the NavigatorService to my App.xaml.cs code so that it is injected and stored as a property for use later:</p> 346 347 348 349<pre class="wp-block-code"><code>public partial class App : Application 350{ 351 public App(NavigatorService navigatorService) 352 { 353 InitializeComponent(); 354 NavigatorService = navigatorService; 355 } 356 357 internal NavigatorService NavigatorService { get; } 358 359 private void MenuItem_Clicked(object sender, EventArgs e) 360 { 361 } 362}</code></pre> 363 364 365 366<h2 class="wp-block-heading">Connecting and Navigating from the MenuBar</h2> 367 368 369 370<p>Now, we can use a switch statement to configure the navigation of the BlazorWebView. Let’s add that switch inside the <code>Menuitem_Clicked</code> method:</p> 371 372 373 374<pre class="wp-block-code"><code>private void MenuItem_Clicked(object sender, EventArgs e) 375{ 376 377 var menuItem = (MenuItem)sender; 378 var url = menuItem.Text switch 379 { 380 "Counter" => "/counter", 381 "Weather" => "/weather", 382 _ => "/" 383 }; 384 NavigatorService.NavigationManager.NavigateTo(url); 385 386}</code></pre> 387 388 389 390<figure class="wp-block-image size-full"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1.png"><img loading="lazy" decoding="async" width="832" height="650" src="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1.png" alt="" class="wp-image-11639" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1.png 832w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1-300x234.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1-768x600.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-1-624x488.png 624w" sizes="(max-width: 832px) 100vw, 832px" /></a><figcaption class="wp-element-caption">Application with the new MenuBar</figcaption></figure> 391 392 393 394<p>Now, when we click the various items in the native MenuBar, the browser navigates appropriately.</p> 395 396 397 398<p>For completeness, I removed the side navigation from the MainLayout.razor file so that the application felt more native and didn’t have 2 MenuBars.</p> 399 400 401 402<figure class="wp-block-image size-full"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2.png"><img loading="lazy" decoding="async" width="832" height="650" src="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2.png" alt="" class="wp-image-11640" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2.png 832w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2-300x234.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2-768x600.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/04/image-2-624x488.png 624w" sizes="(max-width: 832px) 100vw, 832px" /></a></figure> 403 404 405 406<h2 class="wp-block-heading">Summary</h2> 407 408 409 410<p>This is just one creative way to connect our Blazor application to .NET MAUI and reuse the code we’ve already built in Blazor. The <a href="https://github.com/csharpfritz/BlazorHybridNavigation" target="_blank" rel="noreferrer noopener">complete source code</a> for this sample is available on my GitHub. I’m working through an entire application for TagzApp, and will share more of my findings in the weeks ahead.</p> 411 412 413 414<p>Have you tried using Blazor content in .NET MAUI, WPF, or Windows Forms? What was your experience? Let me know in the comments below</p> 415 </div><!-- .entry-content --> 416 417 <footer class="entry-meta"> 418 This entry was posted in <a href="https://jeffreyfritz.com/category/net/" rel="category tag">.NET</a>, <a href="https://jeffreyfritz.com/category/net/net-maui/" rel="category tag">.NET MAUI</a>, <a href="https://jeffreyfritz.com/category/uncategorized/" rel="category tag">Uncategorized</a> and tagged <a href="https://jeffreyfritz.com/tag/net-maui/" rel="tag">.NET MAUI</a>, <a href="https://jeffreyfritz.com/tag/blazor/" rel="tag">blazor</a> on <a href="https://jeffreyfritz.com/2024/04/seamless-navigation-in-net-maui-hybrid-apps-my-net-maui-hybrid-journey-part-1/" title="10:40" rel="bookmark"><time class="entry-date" datetime="2024-04-12T10:40:26-04:00">April 12, 2024</time></a><span class="by-author"> by <span class="author vcard"><a class="url fn n" href="https://jeffreyfritz.com/author/jfritz/" title="View all posts by Jeff" rel="author">Jeff</a></span></span>. </footer><!-- .entry-meta --> 419 </article><!-- #post --> 420 421 <article id="post-11630" class="post-11630 post type-post status-publish format-standard hentry category-asp-net-core category-azure"> 422 <header class="entry-header"> 423 424 <h1 class="entry-title">
425 <a href="https://jeffreyfritz.com/2024/02/from-iot-to-the-cloud-a-net-ecosystem-showcase-with-github-raspberry-pi-and-azure/" rel="bookmark">From IoT to the Cloud: A .NET Ecosystem Showcase with GitHub, Raspberry Pi, and Azure</a> 426 </h1> 427 <div class="comments-link"> 428 <a href="https://jeffreyfritz.com/2024/02/from-iot-to-the-cloud-a-net-ecosystem-showcase-with-github-raspberry-pi-and-azure/#respond"><span class="dsq-postid" data-dsqidentifier="11630 https://jeffreyfritz.com/?p=11630"><span class="leave-reply">Leave a reply</span></span></a> </div><!-- .comments-link --> 429 </header><!-- .entry-header --> 430 431 <div class="entry-content"> 432 <p>As part of a demo to show a complete interconnected .NET ecosystem from IoT to the Cloud, I assembled a demo with the following components:</p> 433<p>– <a href="https://www.raspberrypi.com/products/raspberry-pi-3-model-b/" target="_blank" rel="noopener">Raspberry Pi 3b (ARM32 processor, Wifi, 4GB RAM)</a><br /> 434– <a href="https://smarticase.com/products/smartipi-touch-2" target="_blank" rel="noopener">Touchscreen case from SmartPi-Touch</a><br /> 435– <a href="https://azure.microsoft.com/en-us/products/container-registry/" target="_blank" rel="noopener">Azure Container Registry</a><br /> 436– GitHub Repository<br /> 437– GitHub action<br /> 438– Docker<br /> 439– Docker-Compose<br /> 440– .NET 7 / ASP.NET Core / SignalR Core</p> 441<p>The goal of this demo is to show how a connected IoT device, a Raspberry Pi, can run unattended and receive automatic updates from GitHub, Azure, refresh .NET content on a screen with no interruptions.</p> 442<p>Prior to writing the software for this demo, I followed all of the instructions for the touch-screen and mounted the Raspberry Pi inside the case it provides. This allows me to connect a keyboard and mouse as well as a power cable and work on the Raspberry Pi as a typical desktop workstation.</p> 443<p><em>Note: This blog post are the notes from my construction of this demo. I typically present and show this content on-stage and it is easily my most complex demo with moving pieces on a Raspberry Pi device that I show in my hands, Azure Container Registry, GitHub Codespaces and GitHub actions. To make the demo a little more interesting, I’ll update some CSS (sometimes suggested by the audience) in the GitHub repository using GitHub codespaces on stage and the Raspberry Pi will update with the new look automatically. I’d like to record a video showing this demo and update this post with a link to that demo<br /> 444</em></p> 445<h2>Step 0: Configure an Azure Container Registry</h2> 446<p>I already have one of these at `fritzregistry.azurecr.io`. It was easy enough to configure and deploy with credentials required to access the content. Alternatively, you can also use the <a href="https://github.com/features/packages" target="_blank" rel="noopener">GitHub package repository features</a> and store containers there.</p> 447<h2>Step 1: Configured Docker on the Raspberry Pi</h2> 448<p>These instructions originally appeared at: <a href="https://raspberrytips.com/docker-on-raspberry-pi/" target="_blank" rel="noopener">https://raspberrytips.com/docker-on-raspberry-pi/</a></p> 449<p>On the Raspberry Pi, I downloaded Docker with this command when running as root:</p> 450<pre>curl -sSL https://get.docker.com | sh</pre> 451<p>I then added myself to the `docker` group by running this as my standard user `jfritz`:</p> 452<pre>sudo usermod -aG docker $USER</pre> 453<p>To get connected with the Azure registry, I logged in with this command and specified the registry name, the user id and password displayed on the <strong>Access Keys</strong> panel:</p> 454<p><div id="attachment_11631" style="width: 635px" class="wp-caption aligncenter"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729.png"><img loading="lazy" decoding="async" aria-describedby="caption-attachment-11631" class="size-large wp-image-11631" src="https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729-1024x374.png" alt="Azure Portal and identifying the Access Keys for a container registry" width="625" height="228" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729-1024x374.png 1024w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729-300x109.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729-768x280.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729-624x228.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Pasted-image-20230818165729.png 1137w" sizes="(max-width: 625px) 100v
454w, 625px" /></a><p id="caption-attachment-11631" class="wp-caption-text">Azure Portal and identifying the Access Keys for a container registry</p></div></p> 455<p>I could then login to the registry with these credentials on my Raspberry Pi by executing</p> 456<pre>docker login</pre> 457<p>This generated a `/home/.docker/config.json` file that will be used to login to my private Azure registry.</p> 458<h2>Step 2: Configured Docker-compose</h2> 459<p>For this system, I want to use Watchtower to monitor the container registry and install updates automatically. In order to configure this with those dependencies, I needed Docker-componse. The prerequisites for Docker-Compose are installed with these commands:</p> 460<pre>sudo apt-get install libffi-dev libssl-dev 461sudo apt install python3-dev 462sudo apt-get install -y python3 python3-pip</pre> 463<p>I completed the installation of Docker-Compose with a pip3 install command:</p> 464<pre>sudo pip3 install docker-compose</pre> 465<h2>Step 3: Configure Watchtower</h2> 466<p><a href="https://containrrr.dev/watchtower/" target="_blank" rel="noopener">Watchtower</a> is a container that will watch other containers and gracefully update them when updates are available. I grabbed the watchtower image for ARM devices using this docker command on the Raspberry Pi:</p> 467<pre>docker pull containrrr/watchtower:armhf-latest</pre> 468<p>I built a `docker-compose.yml` file with my desired watchtower configuration:</p> 469<p>
469<script src="https://gist.github.com/csharpfritz/9a259c1703effe7dbefa3be3af9bf226.js"></script>
469</p> 470<p>The `command` argument instructs watchtower to check for updated images every 30 seconds, and the `restart` argument instructs the container to start at startup and always restart when the watchtower stops. More details about how to configure a Docker-compose file are available in their <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/" target="_blank" rel="noopener">documentation</a>.</p> 471<h2>Step 4 : Customize the Dockerfile to build for ARM32</h2> 472<p>The device I have for this scenario is a Raspberry Pi 3B and has an ARM32 processor. That can throw a little wrinkle into things because most systems now target ARM64 processors by default. Not a problem, because there is still support for ARM32 available and we just need to specify it in our deployment scripts.</p> 473<p>The application that I will be running on the device is a <a href="https://github.com/csharpfritz/demopi" target="_blank" rel="noopener">simple ASP.NET Core application</a> that counts the number of times the screen has been touched. In a more complete scenario, there might be a sensor connected or some kiosk screen wired up that presents information and collects data.</p> 474<p>I wrote a Dockerfile for ARM is called `Dockerfile-ARM32` with the following content:</p> 475<p>
475<script src="https://gist.github.com/csharpfritz/396c5581efd551b1217b11e51f6d5a53.js"></script>
475</p> 476<p>There’s an interesting bit in the middle where I copied in the `.git` folder. This allows my application to grab the latest git SHA hash as a bit of a version check for the source code. That SHA is made availalble on the assembly’s `AssemblyInformationalVersionAttribute` attribute value.</p> 477<p>We can then build the container for my ASP.NET Core application to run on the Pi from my Windows workstation using this command:</p> 478<pre>docker build --platform linux/arm -f .\Fritz.DemoPi\Dockerfile-ARM32 . / 479-t fritz.demopi:4 / 480-t fritz.demopi:latest / 481-t fritzregistry.azurecr.io/fritz.demopi:4 / 482-t fritzregistry.azurecr.io/fritz.demopi:latest</pre> 483<p>and then push to my remote registry with:</p> 484<pre>docker push fritzregistry.azurecr.io/fritz.demopi -a</pre> 485<h2>Step 5: Configure Docker on the Pi to run the website</h2> 486<p>By default ASP.NET Core configured port 8080 for the website inside the container. I wrote a quick `docker-compose.yml` script to run my .NET application with all of the configuration I would need:</p> 487<p>
487<script src="https://gist.github.com/csharpfritz/ace672e3d7d5bac55317ca6281e2a3ae.js"></script>
487</p> 488<p>To ensure that the SignalR bits of my demo would work, I removed a privacy extension from the Chromium browser that comes with the Raspberry Pi device.</p> 489<h2>Step 6: Configure the Pi to boot into Chromium for the website</h2> 490<p>In order to configure the Pi device to boot directly into a browser and my application running in the container, I added a file at `~/.config/lxsession/LXDE-pi` called `autostart` with this configuration:</p> 491<pre>@lxpanel --profile LXDE-pi 492@pcmanfm --desktop --profile LXDE-pi 493#@xscreensaver -no-splash 494point-rpi 495@chromium-browser --start-fullscreen --start-maximized http://localhost/</pre> 496<p>Additional options and the instructions I started with are at <a href="https://smarthomepursuits.com/open-website-on-startup-with-raspberry-pi-os/?expand_article=1" target="_blank" rel="noopener">https://smarthomepursuits.com/open-website-on-startup-with-raspberry-pi-os/?expand_article=1</a> where I learned how to write this script.</p> 497<h2>Step 7: Prepare a GitHub action</h2> 498<p>I configured a GitHub action to checkout my code and build in ARM32 format with the Dockerfile established previously. This would also publish the resultant container to my Azure container registry:</p> 499<p>
499<script src="https://gist.github.com/csharpfritz/2d4ff2968b3c4df6dd9985c0600a8792.js"></script>
499</p> 500<p>Success!</p> 501<h2>Summary</h2> 502<p>As changes are made to the GitHub repository, the GitHub action will rebuild the image and deploy it to the Azure Container Registry. Watchtower identifies the update and automatically stops the existing application on the Pi and then deploys a new copy with the same settings. With a little SignalR work, the UI updates seamlessly.</p> 503 </div><!-- .entry-content --> 504 505 <footer class="entry-meta"> 506 This entry was posted in <a href="https://jeffreyfritz.com/category/asp-net-core/" rel="category tag">ASP.NET Core</a>, <a href="https://jeffreyfritz.com/category/azure/" rel="category tag">Azure</a> on <a href="https://jeffreyfritz.com/2024/02/from-iot-to-the-cloud-a-net-ecosystem-showcase-with-github-raspberry-pi-and-azure/" title="11:58" rel="bookmark"><time class="entry-date" datetime="2024-02-26T11:58:44-05:00">February 26, 2024</time></a><span class="by-author"> by <span class="author vcard"><a class="url fn n" href="https://jeffreyfritz.com/author/jfritz/" title="View all posts by Jeff" rel="author">Jeff</a></span></span>. </footer><!-- .entry-meta --> 507 </article><!-- #post --> 508 509 <article id="post-11619" class="post-11619 post type-post status-publish format-standard has-post-thumbnail hentry category-asp-net-core category-uncategorized tag-asp-net-core tag-azure tag-blazor"> 510 <header class="entry-header"> 511 <img width="624" height="390" src="https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-624x390.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="Lesson Pager on the C# in the Cards website" decoding="async" loading="lazy" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-624x390.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-300x188.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-768x481.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager.png 951w" sizes="(max-width: 624px) 100vw, 624px" /> 512 <h1 class="entry-title"> 513 <a href="https://jeffreyfritz.com/2024/02/blazor-and-net-8-how-i-build-a-fast-and-flexible-website/" rel="bookmark">Blazor and .NET 8: How I Built a Fast and Flexible Website</a> 514 </h1> 515 <div class="comments-link"> 516 <a href="https://jeffreyfritz.com/2024/02/blazor-and-net-8-how-i-build-a-fast-and-flexible-website/#comments"><span class="dsq-postid" data-dsqidentifier="11619 https://jeffreyfritz.com/?p=11619">2 Replies</span></a> </div><!-- .comments-link --> 517 </header><!-- .entry-header --> 518 519 <div class="entry-content"> 520 <p>I’ve been working on a new website for my series <a href="https://csharpinthecards.com" target="_blank" rel="noopener">CSharp in the Cards</a>. I built this website in a way that was easy to maintain, flexible and most importantly would respond quickly to requests from visitors. I knew that <a href="https://blazor.net" target="_blank" rel="noopener">Blazor with .NET 8</a> had a static server rendering feature and decided that I wanted to put it to the test. I recently published a new lesson to the website and included a web assembly component to allow for paging and filtering the list of lessons I was pleasantly surprised when I saw the performance dashboards on azure showing that it was handling requests and responding very very quickly.</p> 521<p><div id="attachment_11620" style="width: 967px" class="wp-caption aligncenter"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034.png"><img loading="lazy" decoding="async" aria-describedby="caption-attachment-11620" class="size-full wp-image-11620" src="https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034.png" alt="" width="957" height="473" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034.png 957w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034-300x148.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034-768x380.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/02/Screenshot-2024-02-20-152034-624x308.png 624w" sizes="(max-width: 957px) 100v
521w, 957px" /></a><p id="caption-attachment-11620" class="wp-caption-text">Response times of C# in the Cards after the new episode</p></div></p> 522<p>In this blog post, let’s talk about how I’ve optimized the website for speed and some of the finishing touches that you can put on your Blazor website to make it screaming fast running on a very small instance of Azure App Service.</p> 523<h2>Static Site Rendering – Its Blazor, but easier</h2> 524<p>With .NET 8 there’s a new render mode for Blazor and it’s called static site rendering or SSR. This new render mode ditches all interactivity that we previously had with Blazor server side and Blazor Web Assembly and instead favors delivering HTML and other content from the server to browsers in a high speed manner. We can bolt on other techniques that we know from SEO and website optimization to make this even faster and deliver a great experience for our visitors.</p> 525<p>
525<script src="https://gist.github.com/csharpfritz/7f4a094f16d16614c94bffaa195307a2.js"></script>
525</p> 526<p>The <a href="https://csharpinthecards.com/about" target="_blank" rel="noopener">About page</a> is configured to output a bunch of HTML headers for the SEO folks and the social media pages to be able to present good information about the site. Notice the headers that are added to satisfy the search engines:</p> 527<ul> 528<li>a canonical link element that identifies where the page should be served from</li> 529<li>a keywords meta element with information about what you can find here</li> 530<li>a robots element that tells the search engine crawlers what they can do with the page</li> 531<li>open graph and Twitter meta tags that instruct Twitter, Facebook, LinkedIn, Discord, and other sites about the images, titles, and description of the page</li> 532</ul> 533<p>That’s fine… but there are two other features to notice:</p> 534<ol> 535<li>This is a static page with no data being presented. I’ve tagged it on line 2 with an attribute to allow output caching for 600 seconds (10 minutes). This way the web server doesn’t have to render a new copy when its requested within 10 minutes of a previous request.</li> 536<li>The images references are in webp format. Let’s not overlook this super-compressed format for displaying high-quality images. It might be 2024, but every bit we deliver over the network still matters for performance and the 600×600 portrait picture of myself on this page was compressed nicely:</li> 537</ol> 538<table> 539<tbody> 540<tr> 541<th style="text-align: center;">Original</th> 542<th style="text-align: center;">Compressed</th> 543<th style="text-align: center;"># Difference</th> 544</tr> 545<tr> 546<td style="text-align: center;">PNG</td> 547<td style="text-align: center;">WEBP</td> 548</tr> 549<tr> 550<td style="text-align: center;">450kb</td> 551<td style="text-align: center;">30kb</td> 552<td style="text-align: center;">-93.3%</td> 553</tr> 554</tbody> 555</table> 556<p>93% savings… that’s crazy good and means that your browser is not downloading an extra 420kb it doesn’t need.</p> 557<h2>Data is stored in static files on disk</h2> 558<p>For this simple website I don’t need a big fancy database like SQL Server or Postgres or even MySQL. For this site, I’ve stored all of the data in a simple CSV file on disk. That means that I can edit the list of articles that are available and the metadata that goes with them by just opening the file in Excel and writing new content. This means that when it comes time for me to read data about the list of content that’s available I’m only reading from a very small file on disk and I don’t need to worry about any kind of contention. I also don’t need to worry about any service that’s running to deliver that data because it’s only coming out of a small file on disk that’s read only.</p> 559<p>
559<script src="https://gist.github.com/csharpfritz/427d0e2747a0e3e065df7ee92fb80f6f.js?ts=2"></script>
559</p> 560<p>In this repository class I use the <a href="https://www.nuget.org/packages/LinqToCsv" target="_blank" rel="noopener">LinqToCSV library</a> to open and read all of the content from the file into a Post object in the first method, GetPostsFromDisk. Later, in a public method called GetPosts, you see where I use the in memory cache feature of ASP.NET Core to fetch data from the cache if its available or get it from disk and store it in cache for 30 minutes. I could probably extend this timeout to several hours or even days since the website doesn’t get any new content without uploading a new version of the site.</p> 561<p>The key here is that the meta data about the lessons on the site is loaded and kept in memory. As of episode 9 of the series, the posts.csv file is only 1.4kb so I have no worries about loading its entire contents into memory.</p> 562<p>Don’t forget, in order to add the MemoryCache to your ASP.NET Core application, you need to add this line to your site configuration in the Program.cs file:</p> 563<pre>builder.Services.AddMemoryCache();</pre> 564<p>I could add other cache options like Redis to the site, but with how small the data I want to cache is, I don’t need that sophistication at this point.</p> 565<h2>Pre-rendered Interactive Web Assembly Content is fast… REALLY fast</h2> 566<p>I wanted to add a subset of the lessons to the front page of the website so that you could see the latest six episodes in the video series and scroll back and forth to the other episodes. This should be an interactive component but I still wanted the home page to render quickly and have a fresh speedy response time as you page through and look at the various episodes that are available. The natural way to do this with Blazor is to build a web assembly component that will run on the client and render data as users click on the buttons for that collection of articles.</p> 567<p>I wrote a simple pager component that would receive a collection of lesson data and render cards for each lesson. Since we already know that the collection of lesson data is less than 2kb in size I don’t have a problem sending the entire collection of data into the browser to be rendered.</p> 568<p>
568<script src="https://gist.github.com/csharpfritz/af863192bf3e1e4c5ec45b6b6a3bfab2.js"></script>
568<br /> 569When I use the @rendermode attribute in this code, it forces the render mode to web assembly and ASP.NET will pre-render as well as cache a version of that component’s resultant HTML with the home page. After viewers download the Web Assembly content it will hand control over to web assembly and it will be a fully interactive component for them to be able to work with.</p> 570<p><div id="attachment_11623" style="width: 310px" class="wp-caption aligncenter"><a href="https://jeffreyfritz.com/wp-content/uploads/2024/02/pager.png"><img loading="lazy" decoding="async" aria-describedby="caption-attachment-11623" class="size-medium wp-image-11623" src="https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-300x188.png" alt="Lesson Pager on the C# in the Cards website" width="300" height="188" srcset="https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-300x188.png 300w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-768x481.png 768w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager-624x390.png 624w, https://jeffreyfritz.com/wp-content/uploads/2024/02/pager.png 951w" sizes="(max-width: 300px) 100vw, 300px" /></a><p id="caption-attachment-11623" class="wp-caption-text">Lesson Pager on the C# in the Cards website</p></div></p> 571<p>Blazor lets me build content to be rendered on the web and I get to choose where exactly it should run. It can run in the browser with web assembly it can run statically on the server it can run interactively on the server if I want it to. In this case running as web assembly gives a really nice usability effect that makes it easy for viewers to locate the content they want to watch.</p> 572<h2>Compress the Content from Kestrel</h2> 573<p>By default content that’s delivered from the ASP.NET kestrel web server is uncompressed. We can add brotli compression to the Web server and deliver content in a much smaller package to our visitors with just a few simple lines of code in program.cs. This is something that I think everybody should do with their Internet facing websites:</p> 574<pre>#if (!DEBUG) 575builder.Services.AddResponseCompression(options => 576{ 577options.EnableForHttps = true; 578}); 579#endif</pre> 580<p>Add response compression configures the server so that it will deliver broadly compressed content. In this application I wrap it with the conditional debug detection because hot reload does not work with compression enabled. When we deliver the website to the production web host it will be running in release mode and compression will be enabled.</p> 581<h2>Optimize all the JavaScript and CSS</h2> 582<p>CSS AND JavaScript can be minified and combined to reduce the number and size of downloads for this static content that makes our websites look good. For this website I installed and used the <a href="https://www.nuget.org/packages/LigerShark.WebOptimizer.Core" target="_blank" rel="noopener">WebOptimizer package</a> available on NuGet. My configuration for this looks like the following:</p> 583<p>
583<script src="https://gist.github.com/csharpfritz/e3c3c40fb71574c9334fb300c19151b6.js"></script>
583</p> 584<p>This script bundles the CSS files that were delivered with my website template and minifies the one JavaScript file that I manage with my project.</p> 585<h2>Set long cache-control headers for static content</h2> 586<p>The last thing that I did was set long duration cash dash control headers for static content like images CSS and Javascript files. This is easy to do with just a few more lines of optional configuration when I configure the static file feature inside of ASP.NET Core:</p> 587<p>
587<script src="https://gist.github.com/csharpfritz/582b1b408d1b5480e8eac18a27898173.js"></script>
587</p> 588<h2>Summary</h2> 589<p>This website’s been easy for me to build because I can rely on my normal HTML skills and the plethora of HTML templates and CSS libraries out there to make my website look good. <a href="https://blazor.net" target="_blank" rel="noopener">Blazor</a> helps me to make it interactive render quickly and grow as I add more content to it. my cost in interaction with azure is minimal, as I’m using a <a href="https://azure.microsoft.com/en-us/pricing/details/app-service/linux/" target="_blank" rel="noopener">Basic-2 instance of Azure App Service running Linux</a> to deliver this site.</p> 590 </div><!-- .entry-content --> 591 592 <footer class="entry-meta"> 593 This entry was posted in <a href="https://jeffreyfritz.com/category/asp-net-core/" rel="category tag">ASP.NET Core</a>, <a href="https://jeffreyfritz.com/category/uncategorized/" rel="category tag">Uncategorized</a> and tagged <a href="https://jeffreyfritz.com/tag/asp-net-core/" rel="tag">asp.net core</a>, <a href="https://jeffreyfritz.com/tag/azure/" rel="tag">azure</a>, <a href="https://jeffreyfritz.com/tag/blazor/" rel="tag">blazor</a> on <a href="https://jeffreyfritz.com/2024/02/blazor-and-net-8-how-i-build-a-fast-and-flexible-website/" title="10:47" rel="bookmark"><time class="entry-date" datetime="2024-02-22T10:47:54-05:00">February 22, 2024</time></a><span class="by-author"> by <span class="author vcard"><a class="url fn n" href="https://jeffreyfritz.com/author/jfritz/" title="View all posts by Jeff" rel="author">Jeff</a></span></span>. </footer><!-- .entry-meta --> 594 </article><!-- #post --> 595 596 <article id="post-11567" class="post-11567 post type-post status-publish format-standard has-post-thumbnail hentry category-uncategorized"> 597 <header class="entry-header"> 598 <img width="624" height="160" src="https://jeffreyfritz.com/wp-content/uploads/2023/01/watermark-over-624x160.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="KlipTok logo over a bar graph in a PDF" decoding="async" loading="lazy" srcset="https://jeffreyfritz.com/wp-content/uploads/2023/01/watermark-over-624x160.png 624w, https://jeffreyfritz.com/wp-content/uploads/2023/01/watermark-over-300x77.png 300w, https://jeffreyfritz.com/wp-content/uploads/2023/01/watermark-over.png 705w" sizes="(max-width: 624px) 100vw, 624px" /> 599 <h1 class="entry-title"> 600 <a href="https://jeffreyfritz.com/2023/01/how-to-watermark-annotate-and-digitally-sign-a-pdf-with-ironpdf/" rel="bookmark">How to watermark, annotate, and digitally sign a PDF with IronPDF</a> 601 </h1> 602 <div class="comments-link"> 603 <a href="https://jeffreyfritz.com/2023/01/how-to-watermark-annotate-and-digitally-sign-a-pdf-with-ironpdf/#respond"><span class="dsq-postid" data-dsqidentifier="11567 https://jeffreyfritz.com/?p=11567"><span class="leave-reply">Leave a reply</span></span></a> </div><!-- .comments-link --> 604 </header><!-- .entry-header --> 605 606 <div class="entry-content"> 607 <p>Previously, I shared some code that demonstrated how the <a href="https://jeffreyfritz.com/2023/01/how-i-built-the-first-pdf-report-for-kliptok-using-ironpdf/">new PDF report feature was built for KlipTok</a>. In reviewing the feature, I wanted to give it a little more value. When you read a report generated from KlipTok, I want you to know that it was genuine and accurate data. I started researching the ability to stamp or put an authentic indicator into the report so you could tell it was a real report from KlipTok. I found three techniques with <a href="https://ironpdf.com">IronPDF</a> that were each amazingly easy to implement and gave different experiences when reading the PDF. Let’s take a look at each technique: watermarks, annotations, and digital signatures. <a href="https://jeffreyfritz.com/2023/01/how-to-watermark-annotate-and-digitally-sign-a-pdf-with-ironpdf/#more-11567" class="more-link">Continue reading <span class="meta-nav">→</span></a></p> 608 </div><!-- .entry-content --> 609 610 <footer class="entry-meta"> 611 This entry was posted in <a href="https://jeffreyfritz.com/category/uncategorized/" rel="category tag">Uncategorized</a> on <a href="https://jeffreyfritz.com/2023/01/how-to-watermark-annotate-and-digitally-sign-a-pdf-with-ironp
611df/" title="22:57" rel="bookmark"><time class="entry-date" datetime="2023-01-30T22:57:04-05:00">January 30, 2023</time></a><span class="by-author"> by <span class="author vcard"><a class="url fn n" href="https://jeffreyfritz.com/author/jfritz/" title="View all posts by Jeff" rel="author">Jeff</a></span></span>. </footer><!-- .entry-meta --> 612 </article><!-- #post --> 613 614 <nav id="nav-below" class="navigation"> 615 <h3 class="assistive-text">Post navigation</h3> 616 <div class="nav-previous"><a href="https://jeffreyfritz.com/page/2/" ><span class="meta-nav">←</span> Older posts</a></div> 617 <div class="nav-next"></div> 618 </nav><!-- .navigation --> 619 620 621 </div><!-- #content --> 622 </div><!-- #primary --> 623 624 625 <div id="secondary" class="widget-area" role="complementary"> 626 <aside id="text-5" class="widget widget_text"> <div class="textwidget"><p><strong><a href="http://twitch.tv/csharpfritz">Follow me on Twitch</a>!</strong></p> 627<p><strong><a href="https://mas.to/@csharpfritz" rel="me">Follow me on Mastodon</a></strong></p> 628<p><strong><a href="https://youtube.com/csharpfritz" rel="me">Watch my videos on YouTube</a></strong></p> 629</div> 630 </aside><aside id="search-6" class="widget widget_search"><form role="search" method="get" id="searchform" class="searchform" action="https://jeffreyfritz.com/"> 631 <div> 632 <label class="screen-reader-text" for="s">Search for:</label> 633 <input type="text" value="" name="s" id="s" /> 634 <input type="submit" id="searchsubmit" value="Search" /> 635 </div> 636 </form></aside> 637 <aside id="recent-posts-6" class="widget widget_recent_entries"> 638 <h3 class="widget-title">Recent Posts</h3> 639 <ul> 640 <li> 641 <a href="https://jeffreyfritz.com/2024/07/adding-antivirus-to-net-aspire-systems/">Adding Antivirus to .NET Aspire Systems</a> 642 </li> 643 <li> 644 <a href="https://jeffreyfritz.com/2024/04/seamless-navigation-in-net-maui-hybrid-apps-my-net-maui-hybrid-journey-part-1/">Seamless Navigation in .NET MAUI Hybrid Apps – My .NET MAUI Hybrid Journey (part 1)</a> 645 </li> 646 <li> 647 <a href="https://jeffreyfritz.com/2024/02/from-iot-to-the-cloud-a-net-ecosystem-showcase-with-github-raspberry-pi-and-azure/">From IoT to the Cloud: A .NET Ecosystem Showcase with GitHub, Raspberry Pi, and Azure</a> 648 </li> 649 </ul> 650 651 </aside><aside id="text-3" class="widget widget_text"><h3 class="widget-title">My Courses</h3> <div class="textwidget"><h3>WintellectNow</h3> 652<ul> 653<li><a href="https://wintellectnow.com/Videos/Watch?videoId=getting-started-with-angular-2-and-aspdotnet-core">Get Started with Angular 2 and ASP.NET Core</a></li> 654<li><a href="https://wintellectnow.com/Videos/Watch?videoId=getting-started-with-aspdotnet-core-1.0">Getting Started with ASP.NET Core 1.0</a></li> 655<li><a href="https://wintellectnow.com/Videos/Watch?videoId=docker-for-aspdotnet-core-developers">Docker for ASP.NET Core Developers</a></li> 656<li><a href="https://www.wintellectnow.com/Videos/Watch?videoId=aspdotnet-core-middleware">ASP.NET Core Middleware</a></li><li><a href="https://www.wintellectnow.com/Videos/Watch?videoId=building-aspdotnet-core-web-uis-with-razor">Building ASP.NET Core Web UIs with Razor</a></li><li><a href="https://www.wintellectnow.com/Videos/Watch?videoId=authentication-and-authorization-in-aspdotnet-core">Authentication and Authorization in ASP.NET Core</a></li><li><a href="https://www.wintellectnow.com/Videos/Watch?videoId=net-standard-for-mere-mortals">.NET Standard for Mere Mortals</a></li> 657</ul> 658<img src="/wp-content/uploads/2014/11/pslogo_3.png" alt="Pluralsight"/> 659<br/> 660<ul> 661<li><a href="http://www.pluralsight.com/courses/mobilize-aspdotnet-web-forms">Mobilize Your ASP.NET Web Forms</a></li> 662 <li><A HREF="http://www.pluralsight.com/courses/one-aspdotnet-from-scratch">One ASP.NET from Scratch</a></li> 663</ul></div> 664 </aside> </div><!-- #secondary --> 665 </div><!-- #main .wrapper --> 666 <footer id="colophon" role="contentinfo"> 667 <div class="site-info"> 668 <a class="privacy-policy-link" href="https://jeffreyfritz.com/privacy-policy/" rel="privacy-policy">Privacy Policy</a><span role="separator" aria-hidden="true"></span> <a href="https://wordpress.org/" class="imprint" title="Semantic Personal Publishing Platform"> 669 Proudly powered by WordPress </a> 670 </div><!-- .site-info --> 671 </footer><!-- #colophon --> 672</div><!-- #page --> 673
674<script type="text/javascript" id="disqus_count-js-extra"> 675/* <![CDATA[ */ 676var countVars = {"disqusShortname":"jeffreyfritz"}; 677/* ]]> */ 678</script>
vendor: 1 bytes, line 678
678
679<script type="text/javascript" src="https://jeffreyfritz.com/wp-content/plugins/disqus-comment-system/public/js/comment_count.js?ver=3.1.5" id="disqus_count-js"></script>
vendor: 1 bytes, line 679
679
680<script type="text/javascript" src="https://jeffreyfritz.com/wp-content/themes/twentytwelve/js/navigation.js?ver=20141205" id="twentytwelve-navigation-js"></script>
680 681</body> 682</html> 683<!-- 684Performance optimized by Redis Object Cache. Learn more: https://wprediscache.com 685 686Retrieved 1080 objects (357 KB) from Redis using Predis (v2.1.2). 687-->
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.