diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..b5d6dfb3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 +FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 + +# VARIANT can be either 'hugo' for the standard version or 'hugo_extended' for the extended version. +ARG VARIANT=hugo_extended +# VERSION can be either 'latest' or a specific version number +ARG VERSION=latest + +# Download Hugo +RUN case ${VERSION} in \ + latest) \ + export VERSION=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}') ;;\ + esac && \ + echo ${VERSION} && \ + case $(uname -m) in \ + aarch64) \ + export ARCH=ARM64 ;; \ + *) \ + export ARCH=64bit ;; \ + esac && \ + echo ${ARCH} && \ + wget -O ${VERSION}.tar.gz https://github.com/gohugoio/hugo/releases/download/v${VERSION}/${VARIANT}_${VERSION}_Linux-${ARCH}.tar.gz && \ + tar xf ${VERSION}.tar.gz && \ + mv hugo /usr/bin/hugo + +# Hugo dev server port +EXPOSE 1313 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..12d6163f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/hugo +{ + "name": "Hugo", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "html.format.templating": true + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "bungcip.better-toml", + "davidanson.vscode-markdownlint", + "GitHub.vscode-pull-request-github" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 1313 + ], + "remoteUser": "vscode", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "" + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/go:1": {}, + "ghcr.io/devcontainers/features/hugo:1": {}, + "ghcr.io/devcontainers/features/node:1": {} + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1fbdaf78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf +*.gif binary +*.jpeg binary +*.png binary +*.gz binary +*.jar binary \ No newline at end of file diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml new file mode 100644 index 00000000..149fb31b --- /dev/null +++ b/.github/workflows/hugo.yml @@ -0,0 +1,73 @@ +# Sample workflow for building and deploying a Hugo site to GitHub Pages +name: Deploy Hugo site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +# Default to bash +defaults: + run: + shell: bash + +jobs: + # Build job + build: + runs-on: ubuntu-latest + env: + HUGO_VERSION: 0.151.0 + steps: + - name: Install Hugo CLI + run: | + wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ + && sudo dpkg -i ${{ runner.temp }}/hugo.deb + - name: Install Dart Sass + run: sudo snap install dart-sass + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v3 + - name: Install Node.js dependencies + run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" + - name: Build with Hugo + env: + # For maximum backward compatibility with Hugo modules + HUGO_ENVIRONMENT: production + HUGO_ENV: production + run: | + hugo \ + --minify \ + --baseURL "${{ steps.pages.outputs.base_url }}/" + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./public + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f551fe7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +public/ +/node_modules/ +/resources/_gen/ + +## Locking files for when working with local servers +.hugo_build.lock +package-lock.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..d4117c41 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start local Hugo server", + "type": "shell", + "command": "hugo serve", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..51c15c7e --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,23 @@ +cff-version: 1.2.0 +title: ALGuidelines.dev +abstract: | + "Microsoft ALGuidelines - A Community driven project. + + Best Practices and Design Patterns for the AL Language." +keywords: + - AL + - "Design Patterns" + - "Best Practices" +authors: + - given-names: Eric + family-names: Wauters + - given-names: Arend-Jan + family-names: Kauffmann + - given-names: Henrik + family-names: Helgesen + orcid: https://orcid.org/0000-0002-3281-6712 + - given-names: Jeremy + family-names: Vyska +license: MIT +url: "https://alguidelines.dev" +repository-code: "https://github.com/microsoft/alguidelines" \ No newline at end of file diff --git a/README.md b/README.md index 5cd7cecf..e41ff2b1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,29 @@ -# Project +# alguidelines.dev Site Source Repo -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. +This is the source for the [alguidelines.dev](https://alguidelines.dev) +website. +# Business Central Design Patterns & Best Practices -As the maintainer of this project, please make a few updates: +This site is meant to house some of the community's knowledge about Microsoft Dynamics 365 Business Central AL Development best practices, particularly around hosting Design Patterns. -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +Please see the [ALGuidelines.dev](https://alguidelines.dev/) site for the full information about this project. +## Repo organization + +This is a [hugo](https://gohugo.io) statically-generated site, hosted +on [GitHub Pages](https://https://pages.github.com). The site is automatically built by +GitHub Actions (see regen-gh-pages.yml). + +All site content is stored in the `content` directory in markdown format. + +```text +content/en +├── _index.html # Landing page +├── author # Info about blog authors +├── blog # Blog posts +├── docs # Documentation pages +└── search.md +``` ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a @@ -24,6 +38,25 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +### Setup + +**Run Hugo server** +``` +$ hugo server +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +``` +You can visit [localhost:1313](http://localhost:1313/) in browser to preview website. + +### Making changes + +1. Create a new branch +2. Make your changes +3. Verify your changes locally with Hugo server +4. Commit and push your changes to the branch +5. Raise a PR to main branch +6. Once PR is merged, your changes would be live on the site + ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 00000000..00e77bd7 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/assets/icons/SiteLogo.png b/assets/icons/SiteLogo.png new file mode 100644 index 00000000..0fbe057a Binary files /dev/null and b/assets/icons/SiteLogo.png differ diff --git a/assets/icons/favicon.ico b/assets/icons/favicon.ico new file mode 100644 index 00000000..591e901a Binary files /dev/null and b/assets/icons/favicon.ico differ diff --git a/assets/icons/logo.svg b/assets/icons/logo.svg new file mode 100644 index 00000000..3b76e50c --- /dev/null +++ b/assets/icons/logo.svg @@ -0,0 +1 @@ +AL logo_VS_smallest version_v3 \ No newline at end of file diff --git a/assets/scss/_nav.scss b/assets/scss/_nav.scss new file mode 100644 index 00000000..6a0428b6 --- /dev/null +++ b/assets/scss/_nav.scss @@ -0,0 +1,218 @@ +// +// Main navbar +// + +.td-navbar-cover { + background: $primary; + + @include media-breakpoint-up(md) { + background: transparent !important; + + .nav-link { + text-shadow: 1px 1px 2px $dark; + } + + } + + &.navbar-bg-onscroll .nav-link { + text-shadow: none; + } +} + +.navbar-bg-onscroll { + background: $primary !important; + opacity: inherit; +} + +.td-navbar { + background: #212121; + min-height: 4rem; + margin: 0; + z-index: 32; + + @include media-breakpoint-up(md) { + position: fixed; + top: 0; + width: 100%; + } + + + .navbar-brand { + text-transform: none; + text-align: middle; + + .nav-link { + display: inline-block; + margin-right: -30px; + } + + svg { + display: inline-block; + margin: 0 10px; + height: 30px; + } + } + + .nav-link { + text-transform: none; + font-weight: $font-weight-bold; + } + + .td-search-input { + border: none; + color: $navbar-dark-color; + @include placeholder { + color: $navbar-dark-color; + } + } + + .dropdown { + min-width: 100px; + } + + @include media-breakpoint-down(md) { + padding-right: .5rem; + padding-left: .75rem; + + .td-navbar-nav-scroll { + max-width: 100%; + height: 2.5rem; + margin-top: .25rem; + overflow: hidden; + font-size: .875rem; + + .nav-link { + padding-right: .25rem; + padding-left: 0; + } + + .navbar-nav { + padding-bottom: 2rem; + overflow-x: auto; + white-space: nowrap; + -webkit-overflow-scrolling: touch; + + } + } + } +} + +// Icons +#main_navbar { + li i { + padding-right: 0.5em; + + &:before { + display: inline-block; + text-align: center; + min-width: 1em; + } + } + .alert { + background-color: inherit; + padding:0; + color: $secondary; + border: 0; + font-weight: inherit; + + &:before { + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + content: "\f0d9"; + padding-left: 0.5em; + padding-right: 0.5em; + } + } +} + +// Foldable sidebar menu +nav.foldable-nav { + + &#td-section-nav { + position: relative; + } + + &#td-section-nav label { + margin-bottom: 0; + width: 100%; + } + + .td-sidebar-nav__section, .with-child ul { + list-style: none; + padding: 0; + margin: 0; + } + + .ul-1 > li { + padding-left: 1.5em; + } + + ul.foldable { + max-height: 0; + overflow: hidden; + transition: max-height 0.5s cubic-bezier(0, 1, 0, 1); + } + + input:checked ~ ul.foldable { + max-height: 100000vmax; + transition: max-height 1s ease-in-out; + } + + input[type=checkbox] { display: none; } + + .with-child, .without-child { + position: relative; + padding-left: 1.5em; + } + + .ul-1 .with-child > label:before { + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + font-family: "Font Awesome 5 Free"; font-weight: 900; content: "\f0da"; + position: absolute; + left: 0.1em; + padding-left: 0.4em; + padding-right: 0.4em; + font-size: 1em; + color: $gray-900; + transition: all 0.5s; + &:hover{ + transform: rotate(90deg); + } + } + + .ul-1 .with-child > input:checked ~ label:before { + color: $primary; + transform: rotate(90deg); + transition: transform 0.5s; + } + + .with-child ul { margin-top: 0.1em; } + +} + +@media (hover: hover) and (pointer: fine) { + + nav.foldable-nav { + + .ul-1 .with-child > label:hover:before { + color: $primary; + transform: rotate(30deg); + transition: transform 0.5s; + } + + .ul-1 .with-child > input:checked ~ label:hover:before { + color: $primary; + transform: rotate(60deg) !important; + transition: transform 0.5s; + } + } +} diff --git a/assets/scss/_sidebar-toc.scss b/assets/scss/_sidebar-toc.scss new file mode 100644 index 00000000..ff6fc52c --- /dev/null +++ b/assets/scss/_sidebar-toc.scss @@ -0,0 +1,65 @@ +// +// Right side toc +// +.td-sidebar-toc { + border-left: 1px solid $border-color; + + @supports (position: sticky) { + position: sticky; + top: 4rem; + height: calc(100vh - 4rem); + overflow-y: auto; + } + + order: 2; + padding-top: 0.75rem; + padding-bottom: 1.5rem; + vertical-align: top; + +} + +.td-page-meta { + a { + display: block; + font-weight: $font-weight-medium; + } +} + +.td-toc { + padding-top: 5px; + margin-top: 10px; + border-top: 1px solid $black; + + a { + display: block; + font-weight: $font-weight-medium; + padding-bottom: .25rem; + } + + li { + list-style: none; + display: block; + } + + li li { + margin-left: 0.5rem; + } + + #TableOfContents { + // Hugo's ToC is a mouthful, this can be used to style the top level h2 entries. + > ul > li > ul > li > a {} + + a { + color: $gray-600; + + &:hover { + color: $blue; + text-decoration: none; + } + } + } + + ul { + padding-left: 0; + } +} diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss new file mode 100644 index 00000000..e165c6a4 --- /dev/null +++ b/assets/scss/_styles_project.scss @@ -0,0 +1,7 @@ +.td-page-meta--child { display: none !important; } +.td-page-meta--project-issue { display: none !important; } + +.td-content pre code { + font-family: Consolas, "Courier New", monospace; + size: 115%; +} \ No newline at end of file diff --git a/assets/scss/_variables_project.scss b/assets/scss/_variables_project.scss new file mode 100644 index 00000000..bb13042b --- /dev/null +++ b/assets/scss/_variables_project.scss @@ -0,0 +1,728 @@ +/* + +Add styles or override variables from the theme here. + +*/ + +@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap'); + +body { + font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif !important; + font-weight:500 !important; +} + +$display1-weight: 500 !default; +$display2-weight: 100 !default; + +$primary: #1f2a43 !default; +$primary-light: lighten($primary, 90%) !default; +$secondary: #2d70de !default; +$light: rgb(255, 255, 255) !default; +$grey: #888 !default; +$orange: $secondary; + +ul, ol { + padding-left: 2em; +} + +.navbar-logo { + height: 2em; +} + +.nav-shadow { + box-shadow: 0 2px 2px -2px rgba(0,0,0,.2); +} + +.navbar-brand { + color: #008489 !important +} + +.navbar-bg-onscroll { + box-shadow: 0 2px 2px -2px rgba(0,0,0,.2); + #docsy-top { + display: block !important; + } +} + +.control-bar { + display: none; +} + +.td-content pre { + word-wrap: normal; + background-color: #f8f9fa; + padding: 1rem; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0.25rem; +} + +.highlight > pre { + border: 0px; +} + +.td-content blockquote { + border-radius: .15rem; +} + +.notice.info { + border-left-color: #6bf; + background-color: rgba(102,187,255,.1); +} + +.notice.warning { + border-left-color: #fd6; + background-color: rgba(255,221,102,.1); +} + +.notice.danger { + border-left-color: #f66; + background-color: rgba(255,102,102,.1); +} + +.notice > ul { + margin-bottom: 0em; +} + +#docsy-search { + background: rgba(190, 185, 185, 0.3); +} +input[type="search"]::placeholder { + color: #9B9595 !important; +} + +.td-box--secondary p > a:hover { + color: #121314 !important; +} + +#community a { + color: #121314 !important; +} + +.showcase img { + margin: 0 30px; +} + +.td-content > h1 { font-weight: 700; } +.td-content > h2 { font-weight: 700; } +.td-content > h3 { font-weight: 700; } +.td-content > h4 { font-weight: 700; } +.td-content > h5 { font-weight: 700; } +.td-content > h6 { font-weight: 700; } +.td-content > h7 { font-weight: 700; } + +// Custom CSS for homepage + +.hero { + padding: 20px 0px 25px 0px; + color: white; + background: rgb(65, 127, 143); + background: linear-gradient(130deg, #4487a1 50%, #0188a3 70%); + margin-left: -15px; + margin-right: -15px; + position: relative; + overflow: hidden; + + .container { + position: relative; + z-index: 2; + } +} + +@media (min-width: 992px) { + .hero { + padding: 130px 0 100px; + } + + .hero-banner { + transform: scale(1.4) translateY(10%); + } + +} + +@media (min-width: 1170px) { + .hero { + &:before { + content: ''; + width: 60vw; + height: 60vw; + display: block; + background: linear-gradient(90deg, rgb(0, 129, 155) 59%, rgba(0,145,193,1) 70%); + position: absolute; + left: -20vw; + top: -20vw; + transform: rotate(45deg); + border-radius: 10%; + } + + &:after { + content: ''; + width: 20vw; + height: 100vw; + display: block; + background: linear-gradient(-74deg, #007e97 50%, #0091c1 70%); + position: absolute; + left: 55vw; + bottom: -40vw; + z-index: 1; + transform: rotate(45deg); + } + + .container { + position: relative; + z-index: 2; + } + } +} + +.hero-title { + font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif !important; + font-weight: 700 !important; + // margin-top: 80px; + font-size: 56px; +} + +// .hero-banner { +// margin-top: 80px; +// } + +.hero-big { + background: rgb(0,145,193); + background: linear-gradient( + 140deg, + rgba(0,145,193, 1) 0%, + rgba(0,94,113, 1) 100% + ); + padding: 12px 38px; + border: none; + border-radius: 4px; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + font-size: 22px; + font-weight: 600; + color: white; + margin: 10px 0px 40px 0px; +} + +.hero-mid { + background: rgb(0,145,193); + background: linear-gradient(140deg,rgba(0,145,193, 1) 0%, rgba(0,94,113, 1) 100%); + padding: 16px 30px; + border-radius: 4px; + border: none; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: 18px; + font-weight: 600; + color: white; + margin: 30px 0px 10px 0px; + &:hover { + box-shadow: 0 3px 7px #b7b7b7; + } +} + +.body-container { + margin-left: -15px; + margin-right: -15px; +} + +.body-container .row { + padding: 30px 0px; + margin: 20px auto; +} + +.td-navbar { + .nav-link { + font-weight: normal !important; + color: #f4f4f4 !important; + border-bottom: 1px solid transparent; + &:hover { + color: #fff !important; + border-bottom: 1px solid #00b1f7; + } + } +} + +.td-search-input::placeholder { + color: #fff !important; +} + +.section-title { + color: #3b3c4c; + font-weight: 700; + font-size: 46px; + margin: 12px 0px; +} + +.section-text-bold { + color: #3b3c4c; + font-weight: 700; + margin: 12px 0px; + font-size: 18px; + line-height: 1.2; +} + +.section-text { + color: #5a5b75; + font-weight: 600; + margin: 12px 0px; + font-size: 18px; +} + +.wide-image { + // width: 100%; + margin-left: -15px; + margin-right: -15px; +} +.big-waves { + width: 100% !important; + position: relative; + top: -40px; +} + +.push-up { + margin-top: -50px; +} + +.center-text { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.push-up .col-lg-4, .whats-new .col-lg-4, +.community-cards .card-shadow, .community-cards.show-desktop .col-lg-6{ + margin-bottom: 20px; +} + +.card-shadow { + padding: 35px 30px 30px 30px; + text-align: center; + box-shadow: 0 5px 10px 0 rgba(41,26,204,0.12); + border: none; + border-radius: 6px; + height: 100%; +} + +.card-shadow .section-text-bold { + font-size: 22px; +} + +.card-shadow img { + margin: 2px auto; +} + +.card-shadow .section-text { + color: #5a5b75; + font-size: 16px; + line-height: 1.5; +} + +.feature-card { + padding: 20px; + border-radius: 0; + height: 100%; + border: 1px solid #E8E8E8; + background-color: #FFFFFF; +} + +.feature-card .card-header-custom .section-text-bold { + font-size: 20px; + margin: 4px 0 0; +} + +.feature-card p.section-text { + font-size: 15px; +} + +.feature-card p:last-child { + margin-bottom: 0; +} + +.card-header-custom { + display: flex; + flex-direction: row; + align-items: flex-start; + min-height: 58px; +} + +.card-header-custom img { + margin-right: 16px; +} + +.card-header-custom .section-text-bold { + font-size: 18px; + color: #3b3c4c; +} + +.card .section-text { + font-size: 14px; + color: #5a5b75; +} + +.info-card { + text-align: center; + margin: 0px auto 30px auto; +} + +.info-card .section-text-bold { + font-size: 22px; +} + +.info-card .section-text { + font-size: 17px; +} + +.info-card img { + margin: 10px auto; +} + +.whats-new { + background: url("/images/whats-new-bg.webp") no-repeat; + background-size: cover; + background-position: 0 -150px; +} + +.whats-new .card-shadow { + height: 100%; +} + +.whats-new .section-title { + color: white; + margin-bottom: 5px; + margin-top: 4rem +} + +.whats-new .card-shadow { + background: white; + padding: 25px 15px 15px 15px; + min-height: unset; + box-shadow: 0 5px 10px 0 rgba(41,26,204,0.12); +} + +.whats-new .card-shadow:hover { + box-shadow: 5px 10px 30px 0 rgba(41,26,204,0.12); +} + +.whats-new .card-shadow-content { + padding: 0px 12px; +} + +.whats-new .card-shadow button { + padding: 12px 22px !important; +} + +@media (min-width: 992px) { + .whats-new .card-shadow .section-text-bold { + min-height: 80px; + } + + .whats-new .card-shadow .section-text { + min-height: 122px; + } +} + + + + +.card-badge { + background: #f2f0fb; + padding: 6px 15px; + text-transform: uppercase; + border-radius: 5px; + color: #5a5b75; + font-weight: 600; + font-size: 16px; +} + +.card-shadow .hero-mid { + width: 100%; + position: relative; + bottom: 0px; +} + +.card-shadow .hero-mid-2 { + width: 100%; + position: relative; + bottom: 0px; +} + +.hero-mid-2 { + background: white; + padding: 12px 22px; + border-radius: 4px; + border: 1px solid #00b1f7; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: 18px; + font-weight: 600; + color: #00b1f7; + margin: 30px 0px 10px 0px; + font-weight: 700; + + &:hover { + background: #55d7ff; + background: linear-gradient(140deg, #55d7ff 0%, #3d98b4 100%); + color: #fff; + box-shadow: 0 3px 7px #b7b7b7; + } +} + +.community-cards { + background: url("/images/color-bg.webp") no-repeat center; + background-size: contain; +} + +.community-cards .card-shadow { + background: white; + min-height: 150px; + padding: 16px 22px; +} + +.community-cards .card-header-custom { + margin-right: auto; + display: flex; + align-items: center; +} + +.community-cards .card-header-custom p.section-text-bold { + margin: 0; +} + +.community-cards .card-shadow img { + margin-left: unset; +} + +.community-cards .card-shadow .section-text { + text-align: left; + font-size: 14px; + margin-top: 4px; +} + +.community-cards a:hover .card-shadow { + box-shadow: 0 10px 11px rgba(41, 26, 204, .12); +} + +.text-highlight { + color: #00b1f7; +} + +.community-cards .row { + padding: 30px 0px; + margin: 20px 2px; +} + +.twitter-card-desktop { + position: relative; + left: 150px; + top: 10px; +} + +.show-mobile { + display: none; +} + +.show-mobile-inline { + display: none; +} + +@media (max-width: 1299px) { + .hero { + background: url("/images/hero-bg.webp") no-repeat; + background-size: cover; + } +} + +@media (max-width: 575px) { + .row { + display: inherit; + } +} + +@media (min-width: 320px) and (max-width: 990px) { + .show-desktop { + display: none; + } + + .show-mobile { + display: block; + } + + .show-mobile-inline { + display: inline; + } + + .hero { + background: url("/images/hero-bg.webp") no-repeat; + background-size: cover; + } + + .hero-section-mobile .hero-title { + font-size: 39px; + } + + .show-mobile-margin { + margin-top: 30px; + } + + .card { + min-height: unset; + padding-bottom: 25px; + } + + .card-shadow { + min-height: unset; + padding-bottom: 25px; + } + + .whats-new { + background: url("/images/whats-new-bg.webp") no-repeat; + background-size: cover; + } + + .community-cards { + background: unset; + } +} + +.feature-section .card { + margin-bottom: 26px; +} + +.feature-section .col-lg-4{ + margin-bottom: 25px; +} + +@media (max-width: 480px) { + .body-container .row { + padding: 30px 8px; + } + + .community-cards { + background: unset; + } +} + +.show-mobile-big { + display: none; +} + +@media (max-width: 767px) { + .show-mobile-big { + display: block; + } + + .show-desktop-big { + display: none; + } + + .hero-section-mobile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + } +} + +.hero-section-mobile .show-mobile-big { + margin-top: 10px; +} + +// {{< tabs >}} +// from https://github.com/alex-shpak/hugo-book +.hidden { + display: none; +} + +.book-tabs { +margin-top: 1rem; +margin-bottom: 1rem; + +border: 1px solid rgba(0, 0, 0, 0.125); +border-radius:.15rem; + +overflow: hidden; + +display: flex; +flex-wrap: wrap; + +label { + display: inline-block; + padding: 0.5rem 1rem; + border-bottom: 1px transparent; + cursor: pointer; +} + +.book-tabs-content { + order: 999; + width: 100%; + border-top: 1px solid #f8f9fa; + padding: 1rem; + display: none; +} + +input[type="radio"]:checked + label { + border-bottom: 2px solid #05b; +} +input[type="radio"]:checked + label + .book-tabs-content { + display: block; +} + +.book-tabs-content > .highlight { + margin: 0; +} + +.book-tabs-content > blockquote.notice { + padding: 1.2em 0em 1.2em 1em; + margin-bottom: 1rem; + color: #616161; + border-left: 6px solid #2D70DE; +} + +.book-tabs-content > blockquote.notice.notice.info { border-left-color: #6bf; } +.book-tabs-content > blockquote.notice.notice.warning { border-left-color: #fd6; } +.book-tabs-content > blockquote.notice.notice.danger { border-left-color: #f66; } +} + +a { + text-decoration: inherit; +} + +.post-img { + display: block; + + > p { + text-align: center; + font-style: italic; + } +} + + +.DocSearch-Hit-title mark { + padding: 0; +} + +.DocSearch-Hit-icon svg { + vertical-align: top; +} + +.DocSearch-Hits mark { + padding: 0; +} + +.DocSearch-Hit-content-wrapper { + overflow-y: hidden; +} diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 00000000..5d6a83a4 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,250 @@ +--- +title: "ALGuidelines.dev" +linkTitle: "ALGuidelines.dev" +description: "Design Patterns and Guidelines for AL development for Microsoft Dynamics 365 Business Central" +images: ["images/og-image-fission.png"] +--- + +
+
+
+
+

+ Guidelines and Patterns
+ for Development for
+ Microsoft Dynamics 365 Business Central +

+ +
+ +
+ +
+ +
+
+
+
+ +
+
+
+
+
+

Design Patterns?

+

+ A "Design Pattern" is a software design template to solve common development needs. +

+

+ This site is community run and Microsoft endorsed. +

+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+

Benefits

+
+ +
+
+

+

Repeatability

+

+ By using consistent patterns and best practices, code is far more reusable across diverse projects. +

+
+
+
+
+

+

Teamwork

+

+ If all code uses design patterns and best practices, it's simpler for new team members to ensure + that code is correct, as well as making it easier to jump into a project. +

+
+
+
+
+

+

Performance

+

+ Many design patterns and best practices are also heavily focused on ensuring compliance with + the latest performance recommendations to get the most out of every system. +

+
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+

What's New

+
+
+
+ NEW +

+ Getting Started With Agentic Coding +

+
+

+ Essential concepts and practices for working with AI coding assistants in your AL development workflow. +

+ + + +
+
+
+ +
+
+ GUIDE +

+ Vibe Coding Rules +

+
+

+ AI-optimized coding rules and guidelines designed to enhance the AL developer experience in modern AI-powered IDEs. +

+ +
+
+
+ +
+
+ TOOLS +

+ Agentic Tools +

+
+

+ Model Context Protocol (MCP) servers that enhance AI assistants for Business Central development workflows. +

+ +
+
+
+
+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/content/blog/_index.md b/content/blog/_index.md new file mode 100644 index 00000000..a4d08959 --- /dev/null +++ b/content/blog/_index.md @@ -0,0 +1,5 @@ +--- +title: "AL Guidelines Blog" +linkTitle: "Blog" +type: "blog" +--- \ No newline at end of file diff --git a/content/docs/BestPractices/CustomTelemetry/index.md b/content/docs/BestPractices/CustomTelemetry/index.md new file mode 100644 index 00000000..c2f1eb20 --- /dev/null +++ b/content/docs/BestPractices/CustomTelemetry/index.md @@ -0,0 +1,96 @@ +--- +title: "Custom Telemetry" +tags: ["AL","Telemetry"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by Arend-Jan Kauffmann_ + +## Description +With AL it is possible to emit custom telemetry signals to Azure Application Insights. There are a number of considerations that you should keep in mind when designing custom telemetry signals. + +- Think about it as an API +- Naming conventions and telemetry schema +- Objects emitting telemetry signals +- Candidate data for telemetry +- How customers use telemetry +- Privacy + +## Think about it as an API + +Customers will build analytics and monitoring solutions on top of their telemetry data. + +Therefore, signal must be treated as any other API + +- documented +- versioned +- discoverable +- non-breaking + +## Naming conventions and telemetry schema + +To make it easy for the consumer of telemetry to work with the data, please + +- use **PascalCasing**. This makes all fields in Application Insights look the same (signal logged through the AL LogMessage method will have "al" prefixed to dimension names. +- **Do not use special characters or spaces** for fields/custom dimension keys. This makes the KQL queries so much easier to write +- for custom dimensions, consider using prefixes that helps the telemetry consumer understand where the dimension is coming from (e.g. HttpStatusCode, SqlStatement, ...) + +Consider always having a **"message"** field that expresses in human readable form what the telemetry event is about. +If you do, let message names follow the Object ActionInPastTense pattern +Some examples + +- Web Service Called: +- Email attempt failed +- Authorization to environment succeeded + +```al +local procedure ProcessHttpResponse(var Request: HttpRequestMessage; var Response: HttpResponseMessage) +var + CustomTelemetryDimensions: Dictionary of [Text,Text]; +begin + if Response.HttpStatusCode <> 200 then begin + CustomTelemetryDimensions.Add('Url', Request.GetRequestUri); + CustomTelemetryDimensions.Add('HttpStatusCode', Format(Response.HttpStatusCode)); + CustomTelemetryDimensions.Add('ReasonPhrase', Response.ReasonPhrase); + Session.LogMessage( + 'MyExt0001', + 'Web service call failed', + Verbosity::Error, + DataClassification::SystemMetadata, + TelemetryScope::ExtensionPublisher, + CustomTelemetryDimensions); + end; +end; +``` + +## Objects emitting telemetry signals + +Telemetry data includes information about the object that emitted the telemetry signal. It's recommended to call Session.LogMessage() **from within the object** that causes a situation that you want to have telemetry for. That will make it easier to analyze where exactly in the code an issue occurred. + +Of course it is possible to have a single object as a central place to emit telemetry signals. The telemetry data includes a callstack, so eventually it would be possible to trace back to the exact place where an issue occurred. But that requires a more complicated query, so it would be better to emit telemetry signals right from place in the code where an issue occurred. + +## Candidate data for telemetry + +Telemetry must be **actionable** for the customer. Do not emit signals that they cannot act on (knowing about CPU performance counters on the database is useless if the partner cannot scale the database). + +Also, note that customers pay for data ingestion. So be mindful to not flood their telemetry resources. Consider to use TelemetryScope::ExtensionPublisher by default and only use TelemetryScope::All in case the customer can also act on the data. + +If you do not know where to start, consider using telemetry for deflection. In Dynamics 365 Business Central, they started with signal about authorization (successful/failed) to deflect support cases that was due to disabled users/wrong licenses. + +## How customers use telemetry + +The following are known scenarios for customer telemetry + +- The tenant admin (typically an IT-pro) wants to troubleshoot a performance problem and they need more details than what is provided in the analytics reports in the admin center. +- The customer wants to analyze (and correct) errors happening in the environment (typically an IT-pro) +- The customer wants to analyze usage of features (typically an analytics user, maybe with BI experience) + +Customers typically start in the Application Insights portal and then move on to use more advanced tools for analytics (KQL, Power BI, Excel, ...). Once they have seen the light, they will likely start alerting on telemetry using Azure Monitor Alerts or setting up Power Automate flows. + +Business Central have developed a telemetry maturity model (based on the Gartner BI maturity model) for how organizations can evolve to use telemetry proactively in their business processes. + +## Privacy + +Telemetry must be **privacy compliant**. + +For privacy reasons, events that have a DataClassification other than SystemMetadata aren't sent to Application Insight resources set up on the tenant. During development of your extension, it's good practice to have a privacy review of the use of LOGMESSAGE calls to ensure that customer data isn't mistakenly leaked into Application Insights resources. \ No newline at end of file diff --git a/content/docs/BestPractices/DeleteAll/index.md b/content/docs/BestPractices/DeleteAll/index.md new file mode 100644 index 00000000..3ed623a5 --- /dev/null +++ b/content/docs/BestPractices/DeleteAll/index.md @@ -0,0 +1,27 @@ +--- +title: "DeleteAll" +tags: ["AL","Performance"] +categories: ["Best Practice"] +--- + +_Created by waldo, Described by waldo_ + +## Description + +When you perform a "DeleteAll" when there is nothing to delete, it will still perform a lock. When you for example perform a DeleteAll on an empty table, it will result in a table lock. +Therefore it's good practice to always check if the table is empty when performing a DeleteAll. + +## Bad code + +```al + EmptyTableWLD.SetRange(Code, 'AJ'); + EmptyTableWLD.DeleteAll(true); +``` + +## Good code + +```al + EmptyTableWLD.SetRange(Code, 'AJ'); + if not EmptyTableWLD.IsEmpty() then + EmptyTableWLD.DeleteAll(true); +``` diff --git a/content/docs/BestPractices/SetLoadFields/Index.md b/content/docs/BestPractices/SetLoadFields/Index.md new file mode 100644 index 00000000..e3aae546 --- /dev/null +++ b/content/docs/BestPractices/SetLoadFields/Index.md @@ -0,0 +1,69 @@ +--- +title: "SetLoadFields" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +See the documentation on learn.microsoft.com for more information about [SetLoadFields](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/record/record-setloadfields-method). + +For the performance of your code it is important that you use SetLoadFields as much as possible. + +If you want to retrieve a record from the database to check if the record is available always use SetLoadFields on the primary key fields of the table so only those fields will be retrieved from the database. + +## Bad code + +```AL +if not Item.Get(ItemNo) then + exit(); +``` + +## Good code + +```AL +Item.SetLoadFields("No."); +if not Item.Get(ItemNo) then + exit(); +``` + + +Place the SetLoadFields in the code before the line of the Get (or find). (there is no need to record filter fields in the SetLoadFields because these will be retrieved automatically). +## Bad code + +```AL +Item.SetLoadFields("Item Category Code"); +Item.SetRange("Third Party Item Exists", false); +Item.FindFirst(); +``` + +## Good code + +```AL +Item.SetRange("Third Party Item Exists", false); +Item.SetLoadFields("Item Category Code"); +Item.FindFirst(); +``` + +Place the SetLoadFields in the code before the case statement +## Bad code + +```AL +Item.SetLoadFields("Item Category Code"); +ItemCategoryCode := FindItemCategoryCode; + +case true of + Item.Get(ItemNo): + SetItemCategoryCode(Item, ItemCategoryCode); +end; +``` + +## Good code + +```AL +ItemCategoryCode := FindItemCategoryCode; +Item.SetLoadFields("Item Category Code"); + +case true of + Item.Get(ItemNo): + SetItemCategoryCode(Item, ItemCategoryCode); +end; +``` diff --git a/content/docs/BestPractices/SubscriberCodeunits/index.md b/content/docs/BestPractices/SubscriberCodeunits/index.md new file mode 100644 index 00000000..9f4e61ea --- /dev/null +++ b/content/docs/BestPractices/SubscriberCodeunits/index.md @@ -0,0 +1,201 @@ +--- +title: "Subscriber Codeunits" +tags: ["AL","Performance"] +categories: ["Best Practice"] +--- + +_Created by waldo, Described by waldo_ + +## Description + +In general, subscribers have to be put in codeunits. There are a few performance considerations that you should keep in the back of your minds, when designing such a codeunit. + +- Keep the codeunit as small as possible +- Work with a single instance codeunit +- only subscribe when necessary +- Avoid generic OnInsert/OnModify/OnDelete + +Let's discuss all points + +## Keep the codeunit as small as possible + +Every time a subscriber gets called, a new instance of the codeunit is being loaded in memory, which takes memory and processing power. The smaller the codeunit, the less memory, and the faster it is. + +Therefore, it's suggested to split the subscribers by functionality and avoid putting business logic in the actual codeunit. Tip: put all business logic in an "[Method Codeunit](https://alguidelines.dev/docs/patterns/generic-method-pattern/)". + +Examples: + +- if you app does things on Sales and Purchase, create a Sales-subs codeunit, and a Purchase-subs. +- if you have multiple functionalities in your app (let's call'm modules), create a subs-codeunit per module, and only add the subscribers in there that are necessary for that module. + +### Bad code + +```AL +codeunit 2037325 "Setup Subs" +{ + SingleInstance = true; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + AppId: ModuleInfo; + NameListLbl: Label 'Linked Texts Framework - List', Locked = true; + DescriptionListLbl: Label 'Edit linked texts', Locked = true; + KeyWordListLbl: Label 'LT,Distri,Technical,Functional,Reports', Locked = true; + NameReportLbl: Label 'Linked Texts Framework - Reports', Locked = true; + DescriptionReportLbl: Label 'View linked texts reports', Locked = true; + KeyWordReportLbl: Label 'LT,Distri,Technical,Functional,Reports', Locked = true; + begin + navapp.GetCurrentModuleInfo(AppId); + Sender.Insert(NameListLbl, DescriptionListLbl, KeyWordListLbl, page::"LTE Linked Text List", AppId.Id(), "Manual Setup Category"::General); + Sender.Insert(NameReportLbl, DescriptionReportLbl, KeyWordReportLbl, page::"LTE Linked Texts Reports", AppId.Id(), "Manual Setup Category"::General); + end; + + [EventSubscriber(ObjectType::Codeunit, codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + AppId: ModuleInfo; + NameLayoutLbl: Label 'Report Helper - Layout', Locked = true; + DescriptionLayoutLbl: Label 'Set up or update report layout list', Locked = true; + KeyWordLayoutLbl: Label 'RH,Distri,Technical,Functional,Reports,Layout', Locked = true; + NameCaptionsLbl: Label 'Report Helper - Captions', Locked = true; + DescriptionCaptionsLbl: Label 'Set up or update captions list', Locked = true; + KeyWordCaptionsLbl: Label 'RH,Distri,Technical,Functional,Reports,Captions', Locked = true; + NameFunctionsLbl: Label 'Report Helper - Functions', Locked = true; + DescriptionFunctionsLbl: Label 'Set up or disable functions', Locked = true; + KeyWordFunctionsLbl: Label 'RH,Distri,Technical,Functional,Reports,Functions', Locked = true; + NameDFCLbl: Label 'Report Helper - Default Footer', Locked = true; + DescriptionDFCLbl: Label 'Set up or update default footer', Locked = true; + KeyWordDFCLbl: Label 'RH,Distri,Technical,Functional,Reports,Default,Footer', Locked = true; + begin + navapp.GetCurrentModuleInfo(AppId); + Sender.Insert(NameLayoutLbl, DescriptionLayoutLbl, KeyWordLayoutLbl, page::"RHE Report Layout List", AppId.Id(), "Manual Setup Category"::General); + Sender.Insert(NameCaptionsLbl, DescriptionCaptionsLbl, KeyWordCaptionsLbl, page::"RHE Captions", AppId.Id(), "Manual Setup Category"::General); + Sender.Insert(NameFunctionsLbl, DescriptionFunctionsLbl, KeyWordFunctionsLbl, page::"RHE Functions", AppId.Id(), "Manual Setup Category"::General); + Sender.Insert(NameDFCLbl, DescriptionDFCLbl, KeyWordDFCLbl, page::"RHE Default Footer Card", AppId.Id(), "Manual Setup Category"::General); + end; +} +``` + +### Good code + +Split into 2 codeunits, and move the business logic out. + +```AL +codeunit 2037325 "LTE Setup Subs" +{ + SingleInstance = true; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + RegisterLTEManualSetup: codeunit "Register LTE Manual Setup"; + begin + RegisterLTEManualSetup.RegisterLTEManualSetup(); + end; +} + +codeunit 2037324 "RHE Setup Subs" +{ + SingleInstance = true; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + RegisterRHEManualSetup: codeunit "Register RHE Manual Setup"; + begin + RegisterRHEManualSetup.RegisterRHEManualSetup(); + end; +} + + +``` + +## Work with a single instance codeunit + +To avoid the extra "loading of the content" while a subscriber is being executed, use Single Instance codeunit for subscribers. Do take into account, of course, that it would share the state across the entire session. + +### Bad code + +```AL +codeunit 2037324 "RHE Setup Subs" +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + RegisterRHEManualSetup: codeunit "Register RHE Manual Setup"; + begin + RegisterRHEManualSetup.RegisterRHEManualSetup(); + end; +} +``` + +### Good code + +```AL +codeunit 2037324 "RHE Setup Subs" +{ + SingleInstance = true; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Manual Setup", 'OnRegisterManualSetup', '', false, false)] + local procedure OnRegisterManualSetup(sender: Codeunit "Manual Setup") + var + RegisterRHEManualSetup: codeunit "Register RHE Manual Setup"; + begin + RegisterRHEManualSetup.RegisterRHEManualSetup(); + end; +} +``` + +## only subscribe when necessary + +If possible, only execute the subscriber when really necessary by using Manual Binding. + +### Bad code + +```AL + //subscriber - code should actually only run when Color=Red. + [EventSubscriber(ObjectType::Table, Database::"Just Some Table WLD", 'OnAfterValidateEvent', 'Message 2', false, false)] + local procedure JustDoSomthing(var Rec: Record "Just Some Table WLD"; var xRec: Record "Just Some Table WLD") + begin + if Rec.color <> 'RED' then + exit; //only execute when necessary + + ... + end; + + //business logic + if JustSomeTable.FindSet() then + repeat + JustSomeTable.Validate("Message 2", format(Random(1000))); + until JustSomeTable.Next() < 1; +``` + +### Good code + +```AL + if JustSomeTable.FindSet() then + repeat + if JustSomeTable.Color = 'RED' then + BindSubscription(DemoSubs); + + JustSomeTable.Validate("Message 2", format(Random(1000))); + + if JustSomeTable.Color = 'RED' then + UnbindSubscription(DemoSubs); + until JustSomeTable.Next() < 1; +``` + +## Avoid OnInsert/OnModify/OnDelete + +The reason for this is, that it breaks the batch-calls: + +- Any "OnInsert" subscriber breaks the bulk inserts, simply because it needs to perform an operation after every record that was inserted +- Any "OnModify" subscriber slows down the "ModifyAll", simply because it needs to perform an operation after every record that was modified. I fact: 1 SQL call is turned into a loop of SQL calls. +- Any "OnDelete" subscriber slows down the "DeleteAll", simply because it needs to perform an operation after every record that was deleted. I fact: 1 SQL call is turned into a loop of SQL calls. + +Avoid subscribers to these events. + +## References + +The [Generic Method Pattern](https://alguidelines.dev/docs/patterns/generic-method-pattern/) diff --git a/content/docs/BestPractices/_index.md b/content/docs/BestPractices/_index.md new file mode 100644 index 00000000..9684d0df --- /dev/null +++ b/content/docs/BestPractices/_index.md @@ -0,0 +1,24 @@ +--- +title: "Best Practices" +weight: 3 +description: > + AL Code Best Practices +--- + +This section will be cover things that aren't as simple as Design Patterns, but will help make sure your development is: + +- high-performance +- complies with good designs +- has high maintainability + +## Readability + +Generally, all readability rules are Microsoft style choices only. You can use them to keep consistency with the existing code. + +## Performance + +Some guidelines are simply better for performance considerations rather than readability or anything else. In this section, let's look into some.. + +## Discussion + +All discussion related to Best Practice are to be found on the Github Repo's Discussion pages, found [here](https://github.com/microsoft/alguidelines/discussions/categories/bc-best-practices) diff --git a/content/docs/BestPractices/api-page/index.md b/content/docs/BestPractices/api-page/index.md new file mode 100644 index 00000000..cc23701e --- /dev/null +++ b/content/docs/BestPractices/api-page/index.md @@ -0,0 +1,187 @@ +--- +title: "API Page / Query" +tags: ["AL","API"] +categories: ["Best Practice"] +--- + +_Created by Arend-Jan Kauffmann, Described by Arend-Jan Kauffmann_ + +## Description + +API pages are different from UI pages. They require different properties and don't behave the same. Because API pages are used for integration with external applications, they should be treated as contracts. To achieve this, the following topics are important. + +- Separate API app +- Page properties +- Versioning +- Field properties +- Default fields + +## Separate API app + +It's a good practice to develop API pages in a separate app instead of combining them in a solution. By doing so, it provides better maintainability and is a good way of separation of concerns. + +## Page properties +An API page must define a minimum set of properties. Some of these properties will be part of the URL of the API endpoint. It is recommended to define the properties in the same order as they appear in the URL. + +The properties that must be defined are: + +- PageType = API / QueryType = API +- APIPublisher +- APIGroup +- APIVersion +- EntitySetName +- EntityName +- DelayedInsert (only Page) +- ODataKeyFields + +### APIPublisher +The name of the API publisher is usually the company creating the API. It is the first custom part in the URL for a given endpoint. While the value is case insensitive for HTTP operations (GET, POST, etc.), it is case sensitive when checking for active subscriptions. + +Example: + +```al +APIPublisher = 'contoso'; +``` + +### APIGroup +Sets the group of the API endpoint that page or query is exposed in. In the URL the APIGroup comes after the APIPublisher. It can be used to distinguish different API apps or groups of APIs from each other. While the value is case insensitive for HTTP operations (GET, POST, etc.), it is case sensitive when checking for active subscriptions. + +Example: + +```al +APIGroup = 'app1'; +``` + +### APIVersion +Sets the version(s) of the API endpoint the page or query is exposed in. This property is not mandatory. If it is not specified, then APIs will be exposed as version 'beta'. + +The APIVersion can be set to 'beta' or have the format 'vx.y'. +Example: + +```al +APIVersion = 'beta'; +``` + +or + +```al +APIVersion = 'v1.0'; +``` +#### _Multiple API versions_ +You should __never__ break existing versions. Any breaking change requires to create a new version. + +It is possible to expose an API in multiple versions: +```al +APIVersion = 'beta', 'v1.0'; +``` +This allows to publish a new version of an API app without copying all individual objects and updating the version numbers. Only those API objects that are changed in a new version need to be copied. The other objects only need an addition to the APIVersion property to become available in the new version endpoint. + +### EntitySetName +The EntitySetName is the plural entity name. Think of it as the name of the collection of entities. It is recommended to use camelCasing for this property. The value is case sensitive! + +Example: + +```al +EntitySetName = 'itemCategories'; +``` + +### EntityName +The EntityName sets the singular entity name for the API page or query. This name is not used in the URL. Instead, the EntityName is used in the metadata information. It is recommended to use camelCasing for this property. + +Example: + +```al +EntityName = 'itemCategory'; +``` + +### DelayedInsert +This property is required on an editable API page. It does not apply to an API query object. If ```Editable = false``` is set on the API page, then DelayedInsert is not required. +All APIs pages apply the behavior to first specify all field values and then insert the record at once. + +Example: + +```al +DelayedInsert = true; +``` + +### Full example +Together, the page properties look like: +```al +PageType = API; +APIPublisher = 'contoso'; +APIGroup = 'app1'; +APIVersion = 'v1.0'; +EntitySetName = 'itemCategories'; +EntityName = 'itemCategory'; +DelayedInsert = true; +``` + +The full url will look like: ```https://{url}/api/contoso/app1/v1.0/companies({id})/itemCategories``` + +### ODataKeyFields +The ```EntitySetName``` property in the URL can be extended with an identifier to indicate a single record. + +Example: +``` +.../itemCategories(768b6173-9b19-40ea-8e5d-ce181ec0d645) +``` + +The property ```ODataKeyFields``` defines which field(s) will be used for the identifier value. It is highly recommended to always use the SystemId field for this property. The SystemId field is immutable and will never change for a record. + +The field that is defined in this property should be part of the API page. + +## Field properties +The base structure of an API page is similar to a UI list page: + +```al +layout +{ + area(Content) + { + repeater(records) + { + ... + } + } +} +``` + +When specifying the fields there are some considerations to keep in mind. + +``` +field(displayName; Rec.Name) { } +``` + +There are no mandatory properties. The property ```ApplicationArea``` does not play a role in API pages, so it can be skipped. The property ```Caption``` is also optional and should only be used in case the external application requires captions and the caption should be different from the standard caption as defined in the table. + +The name of the field, in the example above ```displayName```, should be defined in camelCasing. It may not contain spaces, dots, or other special characters. + +It is common use to give certain fields a more describing name. Some examples are: + +* id for field SystemId +* number for field "No." +* displayName for field Name + +## Mandatory fields +These fields should always be part of the API Page: + +* SystemId + * This field should be exposed with the name ```id``` +* SystemModifiedAt + * This field should be exposed with the name ```lastModifiedDateTime```. If you choose a different name, then the webhook functionality will not work properly. + +Example: + +```al +layout +{ + area(Content) + { + repeater(records) + { + field(id; Rec.SystemId) { } + field(lastModifiedDateTime; Rec.SystemModifiedAt) { } + } + } +} +``` diff --git a/content/docs/BestPractices/begin-as-an-afterword/index.md b/content/docs/BestPractices/begin-as-an-afterword/index.md new file mode 100644 index 00000000..5b6bac47 --- /dev/null +++ b/content/docs/BestPractices/begin-as-an-afterword/index.md @@ -0,0 +1,28 @@ +--- +title: "begin as an afterword" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +When `begin` follows `then`, `else`, `do`, it should be on the same line, preceded by one space character. + +## Bad code + +```al + if ICPartnerRefType = ICPartnerRefType::"Common Item No." then + begin + ... + end; +``` + +## Good code + +```al + if ICPartnerRefType = ICPartnerRefType::"Common Item No." then begin + ... + end; +``` diff --git a/content/docs/BestPractices/begin-end/index.md b/content/docs/BestPractices/begin-end/index.md new file mode 100644 index 00000000..d35dea99 --- /dev/null +++ b/content/docs/BestPractices/begin-end/index.md @@ -0,0 +1,65 @@ +--- +title: "Begin-End - Compound Only" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +Only use begin..end to enclose [compound statements](https://docs.microsoft.com/en-us/cpp/c-language/compound-statement-c?view=msvc-170#:~:text=A%20compound%20statement%20%28also%20called%20a%20%22block%22%29%20typically,appear%20at%20the%20head%20of%20a%20compound%20statement.). + +## Example 1 + +### Bad code + +```AL +if FindSet() then begin + repeat + ... + until next() = 0; +end; +``` + +### Good code + +```AL +if FindSet() then + repeat + ... + until next() = 0; +``` + +## Example 2 + +### Bad code + +```AL +if IsAssemblyOutputLine then begin + TestField("Order Line No.", 0); +end; +``` + +### Good code + +```AL +if IsAssemblyOutputLine then + TestField("Order Line No.", 0); +``` + +## Exception + +```AL +// Except for this case +if X then begin + if Y then + //DO SOMETHING; +end else + (not X) +``` + +## Tips + +The [AZ AL Dev Tools/AL Code Outline](https://marketplace.visualstudio.com/items?itemName=andrzejzwierzchowski.al-code-outline) extension adds two new commands to Visual Studio Code to remove begin..end around single statements. + +- `Remove Begin..End around Single Statements from the Active Editor` : removes begin..end around single statement from the current editor +- `Remove Begin..End around Single Statements from the Active Project` : removes begin..end around single statement from the current project diff --git a/content/docs/BestPractices/binary-operator-line-start/index.md b/content/docs/BestPractices/binary-operator-line-start/index.md new file mode 100644 index 00000000..2f7ecae8 --- /dev/null +++ b/content/docs/BestPractices/binary-operator-line-start/index.md @@ -0,0 +1,27 @@ +--- +title: "Binary Operator to Start Line" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +Do not start a line with a binary operator. + +## Bad code + +```AL +"Quantity to Ship" := + Quantity + - "Quantity Shipped" +``` + +## Good code + +```AL +"Quantity to Ship" := + Quantity - + "Quantity Shipped" +``` diff --git a/content/docs/BestPractices/blank-lines/index.md b/content/docs/BestPractices/blank-lines/index.md new file mode 100644 index 00000000..7f1b8efe --- /dev/null +++ b/content/docs/BestPractices/blank-lines/index.md @@ -0,0 +1,115 @@ +--- +title: "When not to use Blank Lines" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +## Description + +Do not use blank lines: + +- at the beginning or end of any functions (after `begin` and before `end`) +- inside multiline expression +- after blank lines + +## Example 1 + +### Bad code + +```al +procedure MATRIX_OnDrillDown(MATRIX_ColumnOrdinal: Integer) +begin + + SetupDrillDownCol(MATRIX_ColumnOrdinal); + DrillDown(false, ValueType); + +end; +``` + +### Good code + +```al +procedure MATRIX_OnDrillDown(MATRIX_ColumnOrdinal: Integer) +begin + SetupDrillDownCol(MATRIX_ColumnOrdinal); + DrillDown(false, ValueType); +end; +``` + +## Example 2 + +### Bad code + +```al +if NameIsValid and + + Name2IsValid +then +``` + +### Good code + +```al +if NameIsValid and + Name2IsValid +then +``` + +## Example 3 + +### Bad code + +```al +var + GLSetup: Record "General Ledger Setup"; + GLSetupRead: Boolean; + + +local procedure GetGLSetup() +begin + if not GLSetupRead then + GLSetup.Get(); + + + GLSetupRead := true; + + + OnAfterGetGLSetup(GLSetup); +end; + + +[IntegrationEvent(false, false)] +local procedure OnAfterGetGLSetup(var GLSetup: Record "General Ledger Setup") +begin +end; +``` + +### Good code + +```al +var + GLSetup: Record "General Ledger Setup"; + GLSetupRead: Boolean; + +local procedure GetGLSetup() +begin + if not GLSetupRead then + GLSetup.Get(); + + GLSetupRead := true; + + OnAfterGetGLSetup(GLSetup); +end; + +[IntegrationEvent(false, false)] +local procedure OnAfterGetGLSetup(var GLSetup: Record "General Ledger Setup") +begin +end; +``` + +## Tips + +The [AZ AL Dev Tools/AL Code Outline](https://marketplace.visualstudio.com/items?itemName=andrzejzwierzchowski.al-code-outline) extension adds two new commands to Visual Studio Code to remove empty duplicate lines. + +- `Remove Empty Lines from the Active Editor` : removes empty duplicate lines from the current editor +- `Remove Empty Lines from the Active Project` : removes empty duplicate lines from the current project diff --git a/content/docs/BestPractices/case-actions/index.md b/content/docs/BestPractices/case-actions/index.md new file mode 100644 index 00000000..5062e53e --- /dev/null +++ b/content/docs/BestPractices/case-actions/index.md @@ -0,0 +1,31 @@ +--- +title: "CASE Action on next line" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +A CASE action should start on a line after the possibility. + +## Bad code + +```AL + case Letter of + 'A': Letter2 := '10'; + 'B': Letter2 := '11'; + end; +``` + +## Good code + +```AL + case Letter of + 'A': + Letter2 := '10'; + 'B': + Letter2 := '11'; + end; +``` diff --git a/content/docs/BestPractices/comments-spacing/index.md b/content/docs/BestPractices/comments-spacing/index.md new file mode 100644 index 00000000..615f9e69 --- /dev/null +++ b/content/docs/BestPractices/comments-spacing/index.md @@ -0,0 +1,23 @@ +--- +title: "Comment Spacing" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +Always start comments with // followed by one space character. + +## Bad code + +```al +RowNo += 1000; //Move way below the budget +``` + +## Good code + +```al +RowNo += 1000; // Move way below the budget +``` diff --git a/content/docs/BestPractices/end-else-pair/index.md b/content/docs/BestPractices/end-else-pair/index.md new file mode 100644 index 00000000..29313176 --- /dev/null +++ b/content/docs/BestPractices/end-else-pair/index.md @@ -0,0 +1,35 @@ +--- +title: "end else pair" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +The `end else` pair should always appear on the same line. + +## Bad code + +```al + if OppEntry.Find('-') then + if SalesCycleStage.Find('-') then begin + ... + end + else + begin + ... + end; +``` + +## Good code + +```al + if OppEntry.Find('-') then + if SalesCycleStage.Find('-') then begin + ... + end else begin + ... + end; +``` diff --git a/content/docs/BestPractices/if-not-find-then-exit/index.md b/content/docs/BestPractices/if-not-find-then-exit/index.md new file mode 100644 index 00000000..6c097864 --- /dev/null +++ b/content/docs/BestPractices/if-not-find-then-exit/index.md @@ -0,0 +1,109 @@ +--- +title: "if not then exit" +tags: ["AL"] +categories: ["Best Practice"] +--- + +_Created by sirhc101, Described by sirhc101_ + +## Description + +In general when we are working with tables we want to make sure, the filtered dataset includes records and does not result in a runtime error, so we use `if` to handle the result of `Find()`, `FindSet()`, `Get()`, etc. +This automatically causes on indent in source code and often the source code does not just contain one but two or more tables involved which leads to multi-level indentation. + +Basically this is a result of bad coding structure but maybe sometimes necessary. On the other hand this causes multiple `end;` usages and leads to the usage of colorization and other helpers to see which `begin` belongs to which `end;`. + +Instead of using `if (Record.FindSet()) then` to fetch records from a database it's good practice to use `if (not Record.FindSet()) then` following by an `exit();` to not further process the source code and make it clear for other developers where they can stop reading in certain cases. + +Furthermore, this more or less automatically leads to smaller and better structured procedures and reduces the complexity of the source code. + +## Bad code + +```al + SalesHeader.Reset(); + SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order); + SalesHeader.SetRange(Status, SalesHeader.Status::Open); + if (SalesHeader.FindSet(false)) then begin + repeat + SalesLine.Reset(); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"::Order); + SalesLine.SetRange("Document No.", SalesHeader."No."); + if (SalesLine.FindSet(true)) then begin + repeat + DoSomething(); + until SalesLine.Next() = 0; + end; + until SalesHeader.Next() = 0; + + DoSomethingElse(); + end; +``` + +or + +```al + SalesLine.Reset(); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"::Order); + if (SalesLine.FindSet(true)) then begin + repeat + case SalesLine."Type" of + SalesLine."Type"::Item: + DoSomethingItem(); + SalesLine."Type"::Resource: + DoSomethingResource(); + end; + until SalesLine.Next() = 0; + end; +``` + +## Good code + +```al + procedure DoSomethingSalesOrder() + var + SalesHeader: Record "Sales Header"; + begin + SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order); + SalesHeader.SetRange(Status, SalesHeader.Status::Open); + if (not SalesHeader.FindSet(false)) then + exit; + + repeat + DoSomethingSalesLine(SalesHeader); + until SalesHeader.Next() = 0; + + DoSomethingElse(); + end; + + procedure DoSomethingSalesLine(var SalesHeader: Record "Sales Header") + var + SalesLine: Record "Sales Line"; + begin + SalesLine.Reset(); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"::Order); + SalesLine.SetRange("Document No.", SalesHeader."No."); + if (not SalesLine.FindSet(true)) then + exit; + + repeat + DoSomething(); + until SalesLine.Next() = 0; + end; +``` + +or + +```al + SalesLine.SetRange("Document Type", SalesHeader."Document Type"::Order); + if (not SalesLine.FindSet(true)) then + exit; + + repeat + case SalesLine."Type" of + SalesLine."Type"::Item: + DoSomethingItem(); + SalesLine."Type"::Resource: + DoSomethingResource(); + end; + until SalesLine.Next() = 0; +``` diff --git a/content/docs/BestPractices/istemporary-table-safeguard/index.md b/content/docs/BestPractices/istemporary-table-safeguard/index.md new file mode 100644 index 00000000..7331b34c --- /dev/null +++ b/content/docs/BestPractices/istemporary-table-safeguard/index.md @@ -0,0 +1,60 @@ +--- +title: "IsTemporary record safeguard" +tags: ["AL"] +categories: ["Best Practice"] +--- + +_Created by Kine, Described by Kine_ + +## Description + +When you are working with temporary tables or real tables, you can have code, where you assume that Record variable is or is not temporary. Best practice is to not assume, but test it to be sure. In history, +many developers went through painful period when they did unwanted "DeleteAll" over real table in production database, because they were only assuming something (mostly it happened only once to them). + +Therefore it is good practice to use [Record.IsTemporary()](https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/record/record-istemporary-method) method to test this predicate, mainly when you are doing destructive action. + +Another case when it is good practice to use this test is when you are subscribing to triggers on table. In most cases, you do not want to run your code when the trigger is running over temporary record. And you cannot assume, that +this specific table will not be used as temporary by someone else. Reacting to the trigger as if it was triggered by real table change could lead to corrupted data or unpredictable errors and the reason could be hard to find. + +## Bad code + +```al + ShouldBeTemporary.DeleteAll(true); +``` + +or + +```al + [EventSubscriber(ObjectType::Table, Database::"Some Table", 'OnAfterInsertEvent', '', false, false)] + local procedure DoSomethingOnAfterInsertSomeTable(var Rec: Record "Some Table") + begin + DoSomething(Rec); + end; +``` + +## Good code + +```al + if ShouldBeTemporary.IsTemporary() then + ShouldBeTemporary.DeleteAll(true); +``` + +or + +```al + if not ShouldBeTemporary.IsTemporary() then + Error(RecNotTemporaryErr); + ShouldBeTemporary.DeleteAll(true); +``` + +or + +```al + [EventSubscriber(ObjectType::Table, Database::"Some Table", 'OnAfterInsertEvent', '', false, false)] + local procedure DoSomethingOnAfterInsertSomeTable(var Rec: Record "Some Table") + begin + if Rec.IsTemporary() then + Exit; + DoSomething(Rec); + end; +``` diff --git a/content/docs/BestPractices/keyboard-shortcuts/index.md b/content/docs/BestPractices/keyboard-shortcuts/index.md new file mode 100644 index 00000000..a9b15863 --- /dev/null +++ b/content/docs/BestPractices/keyboard-shortcuts/index.md @@ -0,0 +1,39 @@ +--- +title: "Keyboard Shortcuts" +tags: ["AL","Productivity"] +categories: ["Best Practice"] +--- + +_Created by Christian Lenz, Described by Christian Lenz_ + +## Description + +To increase developer productivity while coding, use keyboard shortcuts that are available in the specific context to execute actions faster. + +This is a selection of the community's favorites (more to come). + +**Windows** + +| What | Where | How | +|---|---|---| +| Delete word | Editor | CTRL + Backspace | + + +**VS Code** + +| What | Where | How | +|---|---|---| +| Switch Tab | Editor | ALT + | +| Move Line Up/Down | Editor | ALT + | +| Copy Line Below/Above | Editor | ALT + SHIFT + | +| Delete Line | Editor | CTRL + X (without selection) | +| Add Selection To Next Match | Editor | CTRL + D | +| Select All Occurences Of Find Match | Editor | CTRL + SHIFT + L | +| Add Cursors To Line Ends | Editor | SHIFT + ALT + I (after selecting lines) | +| Add Cursor Above/Below | Editor | CTRL + ALT + | +| Place Multiple Cursors Manually | Editor | ALT + Click | +| Fast Scrolling | Editor | ALT + Mouse Wheel | +| Go To Symbol In File | Editor | CTRL + SHIFT + O | +| Breadcrumbs - Open And Select | Editor | CTRL + SHIFT + . | +| Go Back / Forward | Go To Definition | ALT + | + diff --git a/content/docs/BestPractices/keyword-pairs-indentation/index.md b/content/docs/BestPractices/keyword-pairs-indentation/index.md new file mode 100644 index 00000000..b25ea498 --- /dev/null +++ b/content/docs/BestPractices/keyword-pairs-indentation/index.md @@ -0,0 +1,26 @@ +--- +title: "Keyword Pairs - Indentation" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +The `if..then` pair, `while..do` pair, and `for..do` pair must appear on the same line or the same level of indentation. If possible, you can align the lines it is even much more readable. + +## Bad code + +```al + if (x = y) and + (a = b) then +``` + +## Good code + +```al + if (x = y) and + (a = b) + then +``` diff --git a/content/docs/BestPractices/line-start-keywords/index.md b/content/docs/BestPractices/line-start-keywords/index.md new file mode 100644 index 00000000..113e587c --- /dev/null +++ b/content/docs/BestPractices/line-start-keywords/index.md @@ -0,0 +1,31 @@ +--- +title: "Line Start Keywords" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- +_Created by Microsoft, Described by waldo_ + +## Description + +The `end`, `if`, `repeat`, `for`, `while`, `else` and `case` statement should always start a line. + +## Bad code + +```al + if IsContactName then ValidateContactName() + else if IsSalespersonCode then ValidateSalespersonCode() + else if IsSalesCycleCode then ValidatSalesCycleCode(); +``` + +## Good code + +```al + if IsContactName then + ValidateContactName() + else + if IsSalespersonCode then + ValidateSalespersonCode() + else + if IsSalesCycleCode then + ValidatSalesCycleCode(); +``` diff --git a/content/docs/BestPractices/lonely-repeat/index.md b/content/docs/BestPractices/lonely-repeat/index.md new file mode 100644 index 00000000..d15c0945 --- /dev/null +++ b/content/docs/BestPractices/lonely-repeat/index.md @@ -0,0 +1,24 @@ +--- +title: "Lonely Repeat" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +The `repeat` statement should always be alone on a line. + +## Bad code + +```al + if ReservEntry.FindSet() then repeat +``` + +## Good code + +```al + if ReservEntry.FindSet() then + repeat +``` diff --git a/content/docs/BestPractices/named-invocations/index.md b/content/docs/BestPractices/named-invocations/index.md new file mode 100644 index 00000000..395848e4 --- /dev/null +++ b/content/docs/BestPractices/named-invocations/index.md @@ -0,0 +1,29 @@ +--- +title: "Named Invocations" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +When calling an object statically use the Object Name, not the Object Id. + +## Bad code + +```al + Page.RunModal(525, SalesShptLine); +``` + +## Good code + +```al + Page.RunModal(Page::"Posted Sales Shipment Lines", SalesShptLine); +``` + +## Tips + +The [BusinessCentral.LinterCop](https://marketplace.visualstudio.com/items?itemName=StefanMaron.businesscentral-lintercop) extension adds a new rule to check your code for hardcoded object IDs. + +- [LC0012](https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0012): Using hardcoded IDs in functions like Codeunit.Run() is not allowed. diff --git a/content/docs/BestPractices/one-statement-per-line/index.md b/content/docs/BestPractices/one-statement-per-line/index.md new file mode 100644 index 00000000..822c05c1 --- /dev/null +++ b/content/docs/BestPractices/one-statement-per-line/index.md @@ -0,0 +1,41 @@ +--- +title: "One Statement per Line" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +A line of code should not have more than one statement. + +## Example 1 + +### Bad code + +```al + if OppEntry.Find('-') then exit; +``` + +### Good code + +```al + if OppEntry.Find('-') then + exit; +``` + +## Example 2 + +### Bad code + +```al + TotalCost += Cost; TotalAmt += Amt; +``` + +### Good code + +```al + TotalCost += Cost; + TotalAmt += Amt; +``` diff --git a/content/docs/BestPractices/separate-if-and-else/index.md b/content/docs/BestPractices/separate-if-and-else/index.md new file mode 100644 index 00000000..805ff90c --- /dev/null +++ b/content/docs/BestPractices/separate-if-and-else/index.md @@ -0,0 +1,29 @@ +--- +title: "Seperate if and else" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +`if` and `else` statements should be on separate lines. + +## Bad code + +```al + if Atom = '>' then HasLogicalOperator := true else begin + ... + end; +``` + +## Good code + +```al + if Atom = '>' then + HasLogicalOperator := true + else begin + ... + end; +``` diff --git a/content/docs/BestPractices/spacing-binary-operators/index.md b/content/docs/BestPractices/spacing-binary-operators/index.md new file mode 100644 index 00000000..e0bfaac8 --- /dev/null +++ b/content/docs/BestPractices/spacing-binary-operators/index.md @@ -0,0 +1,53 @@ +--- +title: "Spacing Binary Operators" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +There must be exactly one space character on each side of a binary operator such as = + - AND OR =. The parameter comma operator however, should have a space after the comma. + +## Example 1 + +### Bad code + +```al + "Line Discount %" := "Line Discount Amount"/"Line Value"*100; +``` + +### Good code + +```al + "Line Discount %" := "Line Discount Amount" / "Line Value" * 100; +``` + +## Example 2 + +### Bad code + +```al + StartDate := CalcDate('<+'+Format(Days+i)+'D\>',StartDate); +``` + +### Good code + +```al + StartDate := CalcDate('<+' + Format(Days + i) + 'D\>', StartDate); +``` + +## Example 3 + +### Bad code + +```al + StartDate:=0D; // Initialize +``` + +### Good code + +```al + StartDate := 0D; // Initialize +``` diff --git a/content/docs/BestPractices/suggested-abbreviations/index.md b/content/docs/BestPractices/suggested-abbreviations/index.md new file mode 100644 index 00000000..722f1561 --- /dev/null +++ b/content/docs/BestPractices/suggested-abbreviations/index.md @@ -0,0 +1,361 @@ +--- +title: "Suggested Abbreviations" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +Whenever possible, do **not** use abbreviations in variables, functions and objects names. + +If there is no other choice, then use the suggestions below. + +| word | Abbreviation | +|---|---| +| absence | Abs | +| account | Acc | +| accounting | Acc | +| accumulated | Accum | +| action | Act | +| activity | Activ | +| additional | Add | +| address | Addr | +| adjust | Adj | +| adjusted | Adjd | +| adjustment | Adjmt | +| agreement | Agrmt | +| allocation | Alloc | +| allowance | Allow | +| alternative | Alt | +| amount | Amt | +| amounts | Amts | +| answer | Ans | +| applies | Appl | +| application | Appln | +| arrival | Arriv | +| assembly | Asm | +| assemble to order | ATO | +| assignment | Assgnt | +| associated | Assoc | +| attachment | Attmt | +| authorities | Auth | +| automatic | Auto | +| availability | Avail | +| average | Avg | +| ba db. | BA | +| balance | Bal | +| bill of materials | BOM | +| blanket | Blnkt | +| budget | Budg | +| buffer | Buf | +| business | Bus | +| business interaction management | BIM | +| buying | Buy | +| calculate | Calc | +| calculated | Calcd | +| calculation | Calcu | +| calendar | Cal | +| capacity | Cap | +| capacity requirements planning | CRP | +| cash flow | CF | +| cashflow | CF | +| catalog | ctlg | +| category | Cat | +| Central Processing Unit | CPU | +| center | Ctr | +| change | Chg | +| changes | Chgs | +| character | Char | +| characters | Chars | +| charge | Chrg | +| charges | Chrgs | +| check | Chk | +| classification | Class | +| collection | coll | +| column | col | +| comment | Cmt | +| company | Co | +| component | Comp | +| completion | Cmpltn | +| components | Comps | +| composition | Compn | +| compression | Compr | +| concurrent | Concrnt | +| confidential | Conf | +| confirmation | Cnfrmn | +| conflict | Confl | +| consolidate | Consol | +| consolidation | Consolid | +| consumption | Consump | +| contact | Cont | +| container | Cntr | +| contract | Contr | +| contracted | Contrd | +| control | Ctrl | +| controls | Ctrls | +| conversion | Conv | +| correction | Cor | +| correspondence | Corres | +| corresponding | Corresp | +| cost | Cst | +| sold | COGS | +| credit | Cr | +| cumulate | Cumul | +| currency | Curr | +| current | Crnt | +| customer | Cust | +| customer/vendor | CV | +| daily | Dly | +| dampener | Damp | +| database management system | DBMS | +| date | D | +| definition | Def | +| demonstration | Demo | +| department | Dept | +| department/project | DP | +| depreciation | Depr | +| description | Desc | +| detail | Dtl | +| detailed | Dtld | +| details | Dtls | +| deviation | Dev | +| difference | Diff | +| dimension | Dim | +| direct | Dir | +| discount | Disc | +| discrete | Discr | +| distribute | Distr | +| distributed | Distrd | +| distributor | Distbtr | +| distribution | Distrn | +| document | Doc | +| duplicate | Dupl | +| entered | Entrd | +| engineering | Engin | +| exchange | Exch | +| excluding | Excl | +| execute | Exec | +| expected | Expd | +| expedited | Exped | +| expense | Exp | +| expression | Expr | +| expiration | Expir | +| extended | Ext | +| explode | Expl | +| export | Expt | +| final | Fnl | +| finance | Fin | +| fiscal | Fisc | +| finished | Fnshd | +| fixed asset | FA | +| forward | Fwd | +| freight | Frt | +| general | Gen | +| general ledger | GL | +| group | Gr | +| header | Hdr | +| history | Hist | +| holiday | Hol | +| human resource | HR | +| identification | ID | +| import | Imp | +| inbound | Inbnd | +| including | Incl | +| included | Incld | +| incoming | Incmg | +| independent software vendor | ISV | +| industry | Indust | +| information | Info | +| initial | Init | +| Intrastat | Intra | +| interaction | Interact | +| integration | Integr | +| interest | Int | +| Interim | Intm | +| internal protocol | IP | +| inventory | Invt | +| inventoriable | Invtbl | +| invoice | Inv | +| invoiced | Invd | +| item tracking | IT | +| journal | Jnl | +| language | Lang | +| ledger | Ledg | +| level | Lvl | +| line | Ln | +| list | Lt | +| local currency | LCY | +| location | Loc | +| mailing | Mail | +| maintenance | Maint | +| management | Mgt | +| manual | Man | +| manufacturing | Mfg | +| manufacturer | Mfr | +| material | Mat | +| marketing | Mktg | +| maximum | Max | +| measure | Meas | +| message | Msg | +| minimum | Min | +| miscellaneous | Misc | +| modify | Mod | +| month | Mth | +| negative | Neg | +| non-inventoriable | NonInvtbl | +| notification | Notif | +| number | No | +| numbers | Nos | +| object | Obj | +| operating | Oper | +| opportunity | Opp | +| order | Ord | +| orders | Ords | +| original | Orig | +| organization | Org | +| outbound | Outbnd | +| Outgoing | Outg | +| output | Out | +| outstanding | Outstd | +| overhead | Ovhd | +| payment | Pmt | +| percent | Pct | +| personnel | Persnl | +| physical | Phys | +| picture | Pic | +| planning | Plng | +| posted | Pstd | +| posting | Post | +| positive | Pos | +| precision | Prec | +| prepayment | Prepmt | +| product | Prod | +| production | Prod | +| production order | ProdOrd | +| project | Proj | +| property | Prop | +| prospect | Prspct | +| purchase | Purch | +| purchases | Purch | +| purchaser | Purchr | +| purchase order | PurchOrd | +| quality | Qlty | +| quantity | Qty | +| questionnaire | Questn | +| quote | Qte | +| radio frequency | RF | +| range | Rng | +| receipt | Rcpt | +| received | Rcd | +| record | Rec | +| records | Recs | +| reconcile | Recncl | +| reconciliation | Recon | +| recurring | Recur | +| reference | Ref | +| register | Reg | +| registration | Regn | +| registered | Regd | +| relation | Rel | +| relations | Rels | +| relationship | Rlshp | +| release | Rlse | +| released | Rlsd | +| remaining | Rem | +| reminder | Rmdr | +| replacement | Repl | +| replenish | Rplnsh | +| replenishment | Rplnsht | +| report | Rpt | +| represent | Rep | +| represented | Repd | +| request | Rqst | +| required | Reqd | +| requirement | Reqt | +| requirements | Reqts | +| requisition | Req | +| reserve | Rsv | +| reserved | Rsvd | +| reservation | Reserv | +| resolution | Resol | +| resource | Res | +| response | Rsp | +| responsibility | Resp | +| retain | Rtn | +| retained | Rtnd | +| return | Ret | +| returns | Rets | +| revaluation | Revaln | +| reverse | Rev | +| review | Rvw | +| round | Rnd | +| rounded | Rndd | +| rounding | Rndg | +| route | Rte | +| routing | Rtng | +| routine | Rout | +| sales & receivables | Sales | +| safety | Saf | +| schedule | Sched | +| second | Sec | +| segment | Seg | +| select | Sel | +| selection | Selctn | +| sequence | Seq | +| serial | Ser | +| serial number | SN | +| service | Serv | +| sheet | Sh | +| shipment | Shpt | +| source | Src | +| special | Spcl | +| specification | Spec | +| specifications | Specs | +| standard | Std | +| frequency | SF | +| statement | Stmt | +| statistical | Stat | +| statistics | Stats | +| stock | Stk | +| stockkeeping unit | SKU | +| stream | Stm | +| structured query language | SQL | +| subcontract | Subcontr | +| subcontracted | Subcontrd | +| subcontracting | Subcontrg | +| substitute | Sub | +| substitution | Subst | +| suggest | Sug | +| suggested | Sugd | +| suggestion | Sugn | +| summary | Sum | +| suspended | Suspd | +| symptom | Sympt | +| synchronize | Synch | +| temporary | Temp | +| total | Tot | +| transaction | Transac | +| transfer | Trans | +| translation | Transln | +| tracking | Trkg | +| troubleshoot | Tblsht | +| troubleshooting | Tblshtg | +| unit of measure | UOM | +| unit test | UT | +| unrealized | Unreal | +| unreserved | Unrsvd | +| update | Upd | +| valuation | Valn | +| value | Val | +| value added tax | VAT | +| variance | Var | +| vendor | Vend | +| warehouse | Whse | +| web shop | WS | +| worksheet | Wksh | +| g/l | GL | +| % | Pct | +| 3-tier | Three-Tier | +| Outlook Synch | Osynch | diff --git a/content/docs/BestPractices/unnecessary-else/index.md b/content/docs/BestPractices/unnecessary-else/index.md new file mode 100644 index 00000000..4dd6d1bd --- /dev/null +++ b/content/docs/BestPractices/unnecessary-else/index.md @@ -0,0 +1,34 @@ +--- +title: "Unnecessary else" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +`else` should not be used when the last action in the `then` part is an `exit`, `break`, `skip`, `quit`, `error`. + +## Bad code + +```al + procedure SomeProcedure() + begin + if IsAdjmtBinCodeChanged() then + Error(AdjmtBinCodeChangeNotAllowedErr, ...) + else + Error(BinCodeChangeNotAllowedErr, ...); + end; +``` + +## Good code + +```al + procedure SomeProcedure() + begin + if IsAdjmtBinCodeChanged() then + Error(AdjmtBinCodeChangeNotAllowedErr, ...) + Error(BinCodeChangeNotAllowedErr, ...); + end; +``` diff --git a/content/docs/BestPractices/unnecessary-truefalse/index.md b/content/docs/BestPractices/unnecessary-truefalse/index.md new file mode 100644 index 00000000..57d6864b --- /dev/null +++ b/content/docs/BestPractices/unnecessary-truefalse/index.md @@ -0,0 +1,38 @@ +--- +title: "Unnecessary true/false" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description +Do not use `true` or `false` keywords unnecessarily if the expression is already an logical expression. + +## Example 1 + +### Bad code + +```al + if IsPositive() = true then +``` + +### Good code + +```al + if IsPositive() then +``` + +## Example 2 + +### Bad code + +```al + if Complete <> true then +``` + +### Good code + +```al + if not Complete then +``` diff --git a/content/docs/BestPractices/variable-naming/index.md b/content/docs/BestPractices/variable-naming/index.md new file mode 100644 index 00000000..242d1d15 --- /dev/null +++ b/content/docs/BestPractices/variable-naming/index.md @@ -0,0 +1,63 @@ +--- +title: "Variable Naming" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +Variables that refer to a AL object must contain the objects name, abbreviated where necessary. + +A variable must begin with a capital letter. + +Blanks, periods, and other characters (such as parentheses) that would make quotation marks around a variable necessary must be omitted. + +If a variable is a compound of two or more words or abbreviations, each word or abbreviation should begin with a capital letter. + +## Example 1 + +### Bad code + +```al + WIPBuffer: Record "Job WIP Buffer" +``` + +### Good code + +```al + JobWIPBuffer: Record "Job WIP Buffer" +``` + +## Example 2 + +### Bad code + +```al + Postline: Codeunit "Gen. Jnl.-Post Line"; +``` + +### Good code + +```al + GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line"; +``` + +## Example 3 + +### Bad code + +```al + "Amount (LCY)": Decimal; +``` + +### Good code + +```al + AmountLCY: Decimal; +``` + +## Tips + +The [AL Variable Helper](https://marketplace.visualstudio.com/items?itemName=rasmus.al-var-helper) extension provides Intellisense support to assign correct variable names in AL. diff --git a/content/docs/BestPractices/variables-declarations-order/index.md b/content/docs/BestPractices/variables-declarations-order/index.md new file mode 100644 index 00000000..8f7e2131 --- /dev/null +++ b/content/docs/BestPractices/variables-declarations-order/index.md @@ -0,0 +1,48 @@ +--- +title: "Variables Declarations Order" +tags: ["AL","Readability"] +categories: ["Best Practice"] +--- + +_Created by Microsoft, Described by waldo_ + +## Description + +Variables declarations should be ordered by type. In general, object and complex variable types are listed first followed by simple variables. The order should be: + +- Record +- Report +- Codeunit +- XmlPort +- Page +- Query +- Notification +- BigText +- DateFormula +- RecordId +- RecordRef +- FieldRef +- FilterPageBuilder + +(Ref: [Microsoft Docs](https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/analyzers/codecop-aa0021)) + +## Bad code + +```al + StartingDateFilter: Text; + Vendor: Record Vendor; +``` + +## Good code + +```al + Vendor: Record Vendor; + StartingDateFilter: Text; +``` + +## Tips + +The [AZ AL Dev Tools/AL Code Outline](https://marketplace.visualstudio.com/items?itemName=andrzejzwierzchowski.al-code-outline) extension adds two new commands to Visual Studio Code to sorts variables. + +- `Sort Variables in the Active Editor` : sorts variables in the current editor +- `Sort Variables in the Active Project` : sorts variables in the current project diff --git a/content/docs/Contributing/ForkAndPR/ForkedRepro.png b/content/docs/Contributing/ForkAndPR/ForkedRepro.png new file mode 100644 index 00000000..481fd416 Binary files /dev/null and b/content/docs/Contributing/ForkAndPR/ForkedRepro.png differ diff --git a/content/docs/Contributing/ForkAndPR/fork_button.jpg b/content/docs/Contributing/ForkAndPR/fork_button.jpg new file mode 100644 index 00000000..b3888091 Binary files /dev/null and b/content/docs/Contributing/ForkAndPR/fork_button.jpg differ diff --git a/content/docs/Contributing/ForkAndPR/index.md b/content/docs/Contributing/ForkAndPR/index.md new file mode 100644 index 00000000..d8105e04 --- /dev/null +++ b/content/docs/Contributing/ForkAndPR/index.md @@ -0,0 +1,63 @@ ++++ +chapter = true +pre = " " +title = "Guide to Fork & PR" +weight = 100 ++++ + +## Abstract + +AL Guidelines is a community project, and as such YOU are encouraged to submit corrections and new ideas. In order to get your content included, you must submit a pull request to the GitHub Repository (Located here: https://github.com/microsoft/alguidelines\). All Pull Requests are subject to approval by a minimum of three admins. + +If You are toying with an idea, but You aren't ready to create a document just yet, you are encouraged to create a project discussion thread here: https://github.com/microsoft/alguidelines/discussions + +{{% alert title="Warning" color="warning" %}} +This is a warning. +If You haven't worked in collaboration with "external" GitHub repositories before, please familiarize yourself with that process by visiting: + +https://docs.github.com/en/pull-requests/collaborating-with-pull-requests + +{{% /alert %}} + +## Steps +Now that You have decided that You are ready to contribute, these are the steps to take. +{{% alert title="Note" color="info" %}} +You can read more about this process here: + +https://docs.github.com/en/get-started/quickstart/contributing-to-projects + +{{% /alert %}} + +### Step 1: Fork +In order to work on the repository, You must [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the repository. + +By forking the repository, You essentially create a copy into Your own account. + +Start by going to the GitHub Repository (https://github.com/microsoft/alguidelines\), and press the Fork ![Fork Button](./fork_button.jpg "Fork") + +Once You have successfully forked the repository, go to your own GitHub repository : ![Forked Repository Representation](./ForkedRepro.png "Forked repository") + +You are now able to clone your own repository to your local pc and start editing using your favorite editor. [Visual Studio Code](https://code.visualstudio.com/) is perfectly fine for this task. + +{{% alert title="Note" color="info" %}} +You can read more about forking here: + +https://docs.github.com/en/get-started/quickstart/fork-a-repo +{{% /alert %}} + +### Step 2: Branch +While not necessarily a must, it is always good practice to create a branch off of your forked repository. That will allow you to work on multiple contributions at the same time and won't have to wait for pull requests to be approved before you can continue on your next contribution. + +{{% alert title="Note" color="info" %}} +You can read more about Branches here: + +https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches +{{% /alert %}} +### Step 3: Pull Request +Once you are happy with your contribution, it's time to create a pull request to propose changes into the main project! This is the final step in producing a fork of someone else's project, and arguably the most important. If you've made a change that you feel would benefit the community as a whole, you should definitely consider contributing back. + +To do so, head on over to the repository on GitHub where your project lives. For this example, it would be at `https://www.github.com//alguidelines`. You'll see a banner indicating that your branch is one commit ahead of microsoft:main. Click **Contribute** and then **Open a pull request.** + +GitHub will bring you to a page that shows the differences between your fork and the microsoft/alguidelines repository. Click **Create pull request.** + +GitHub will bring you to a page where you can enter a title and a description of your changes. _It's important to provide as much useful information and a rationale for why you're making this pull request in the first place._ The project owners needs to be able to determine whether your change is as useful to everyone as you think it is. Finally, click **Create pull request.** diff --git a/content/docs/Contributing/FormattingTips/index.md b/content/docs/Contributing/FormattingTips/index.md new file mode 100644 index 00000000..bf6f6a18 --- /dev/null +++ b/content/docs/Contributing/FormattingTips/index.md @@ -0,0 +1,126 @@ ++++ +chapter = true +pre = " " +title = "Formatting tips" +weight = 100 ++++ + +# Tips and tricks in terms of working with MarkDown and Hugo + +## Markdown Cheat Sheet +Here you can find a rather interesting Cheat Sheet regarding markdown: [https://www.markdownguide.org/cheat-sheet](https://www.markdownguide.org/cheat-sheet) + +## Code Fences / Syntax highlighting + +The syntax to use codefences is with backticks. If you provide the language after the first block of backticks, github will automatically put that in decent syntax highlighting. So, A simple code fence with AL code, can simply be done by: +```` +```AL + procedure ALGuidelinesRock() + var + Customer: Record Customer; + begin + Customer.Get('10000'); + Customer.Name := 'waldo'; + Customer.Modify(true); + end; +``` +```` + +Results in: +```AL + procedure ALGuidelinesRock() + var + Customer: Record Customer; + begin + Customer.Get('10000'); + Customer.Name := 'waldo'; + Customer.Modify(true); + end; +``` + +## Diagrams with Mermaid + +When providing a documentation, diagrams come in handy. [Mermaid](https://mermaid-js.github.io/mermaid/#/) lets you create diagrams and visualizations using text and code. + +For example the following markdown section: + +```` +```mermaid +classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + } +``` +```` + +Results in: + +```mermaid +classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + } +``` + +Can't wait to get started? Use the Mermaid [Live Editor](https://mermaid.live/edit). + +## Hugo Shortcodes +Since we're using "Hugo", we can use it's shortcode. Here is a reference: [https://gohugo.io/content-management/shortcodes/](https://gohugo.io/content-management/shortcodes/) + +It basically means we are able to use easy notations to do cool things. Let's point out some useful shortcodes: + +### Twitter + +``` +{{}} +``` +makes: +{{< tweet user="waldo1001" id="1458787011024805892" >}} + +### YouTube +``` +{{}} +``` +makes: +{{< youtube QVOMCYitLEc >}} + +### Figure +``` +{{}} +``` +makes: +{{< figure src="http://www.waldo.be/wp-content/uploads/2021/11/business-central-logo.png" title="Business Central" >}} diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariables.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariables.png new file mode 100644 index 00000000..1f76acbe Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariables.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariablesPath.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariablesPath.png new file mode 100644 index 00000000..7c4bb5f6 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/EnvironmentVariablesPath.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/Extract.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/Extract.png new file mode 100644 index 00000000..35cb5f98 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/Extract.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/ExtractToBin.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/ExtractToBin.png new file mode 100644 index 00000000..ba466115 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/ExtractToBin.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/HugoServe.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/HugoServe.png new file mode 100644 index 00000000..569b5098 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/HugoServe.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SearchForEnv.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SearchForEnv.png new file mode 100644 index 00000000..881136cd Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SearchForEnv.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SelectExtended.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SelectExtended.png new file mode 100644 index 00000000..a8adec9c Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SelectExtended.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SystemProperties.png b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SystemProperties.png new file mode 100644 index 00000000..ae8adee9 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/SystemProperties.png differ diff --git a/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/index.md b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/index.md new file mode 100644 index 00000000..7e4b8105 --- /dev/null +++ b/content/docs/Contributing/InstallHugo/ManuallyOnWindows11/index.md @@ -0,0 +1,57 @@ +--- +title: "Manually on Windows 11" +--- + +This guide will walk You thrugh installing Hugo on a Windows 11 PC. For the official install guide, You can wisit . + +## Download Binaries + +The path of least resistance is to just download the .zip file from the official Hugo GitHub Repository [here](https://github.com/gohugoio/hugo/releases). + +The theme that are used, needs the extended version of Hugo, so make sure to download the **extended** version by ensuring the file name starts with `hugo_extended...` + +{{% alert title="info" color="info" %}} +For the purpose of this install guide, we are assuming You will want to follow the "standard" used by Hugo for installing. We will be creating a `Hugo` folder in the root of `C:\`. That folder will have a `Bin` folder for the binaries, and a `Sites` folder for each website You are building. + +Once You are done, You should have a folder structure that looks like this: + +```text +C:\Hugo +├── Bin # Binaries +└┬─ Sites # Hugo Site Repositories + └── alguidelines # Hugo Source for alguidelines.dev +``` + +{{% /alert %}} + +![image](SelectExtended.png) + +After downloading the .zip file, extract the zip-file to `c:\Hugo\Bin` + +![image](Extract.png) + +![image](ExtractToBin.png) + +## Add Hugo to Path + +After copying the binaries to Your PC, You will need to add Hugo binaries to the `%PATH%` system environment variables. + +To do that, search for `environment` + +![image](SearchForEnv.png) + +once you see the `Edit the system environment variables`, open it and select `Environment Variables` + +![image](SystemProperties.png) + +Once the Environment Variables screen is open, highlight the `Path` lines and press the `Edit...` button + +![image](EnvironmentVariables.png) + +Now press `New` and add the `C:\Hugo\Bin` to the path. Press `OK` and `OK` to save the new `Path` + +![image](EnvironmentVariablesPath.png) + +Once complete. You should now be able to preview the Hugo site on by opening a command promt, and open the `C:\Hugo\Sites\alguidelines` folder and execute `Hugo Serve` + +![image](HugoServe.png) \ No newline at end of file diff --git a/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines codespace.mp4 b/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines codespace.mp4 new file mode 100644 index 00000000..173ea8d5 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines codespace.mp4 differ diff --git a/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines walkthrough.mp4 b/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines walkthrough.mp4 new file mode 100644 index 00000000..269de8a0 Binary files /dev/null and b/content/docs/Contributing/InstallHugo/TheShortcut/alguidelines walkthrough.mp4 differ diff --git a/content/docs/Contributing/InstallHugo/TheShortcut/index.md b/content/docs/Contributing/InstallHugo/TheShortcut/index.md new file mode 100644 index 00000000..9f46fc7f --- /dev/null +++ b/content/docs/Contributing/InstallHugo/TheShortcut/index.md @@ -0,0 +1,35 @@ +--- +Title: Devcontainer with VS Code +--- + +## Use a local devcontainer + +If you don't want any local setup (apart from Docker Desktop), but still run your own Hugo instance, you can make use of the preconfigured devcontainer. If you want to learn more about the concept, visit [https://code.visualstudio.com/docs/remote/containers](https://code.visualstudio.com/docs/remote/containers). To use it, you need to take the following steps: + +1. Start [Docker Desktop](https://www.docker.com/products/docker-desktop) and [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers) by right-clicking on the Docker logo in the system tray and selecting "Switch to Linux containers...". If you only see "Switch to Windows containers...", then you are already switched to Linux containers. If anything goes wrong, check if you are on the latest version of Docker Desktop and have [the WSL2 integration enabled](https://docs.docker.com/desktop/windows/wsl/#install). If you have all that in place and it still doesn't work, check the extended installation documentation [here](https://code.visualstudio.com/docs/remote/containers#_installation) +{{% alert title="Consequences of switching" color="info" %}} +When switching to Linux, you will probably see a warning that tells you that you "will not be able to manage the Windows containers until you switch back to Windows containers". That means that the Docker Desktop management GUI can only show either the Windows containers or the Linux containers and if you switch to Linux, you consequently won't see the Windows containers until you switch back. But the Windows containers will continue to run, you won't loose data and you can keep using them e.g. for Business Central development, you just can't manage them through the Docker Desktop GUI +{{% /alert %}} +2. Install the [Remote development extension pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) in Visual Studio Code +3. Run the action "Remote containers: Clone Repository in Container Volume" and select the fork you created. If you haven't done that before, go through the [docs](https://alguidelines.dev/docs/contributing/forkandpr/#step-1-fork). +4. Wait for a bit. When you do this for the first time, it can take a couple of minutes. Next time it will be faster... +5. After a while, you will have VS Code with the cloned repository and the terminal should show something like "Done. Press any key to close the terminal." +6. Run the action "Tasks: Run task" and select "Start local Hugo Server" from the list. If you don't see that entry, you might have to reload your VS Code window and try again +7. After Hugo has generated the site, you will get a notification that offers you to "Open a browser". Click on that and you will see your local instance of the AL guidelines! Again, on the first try it will be a bit slow and sluggish, but the second one should be fast. +8. Now you can make changes and just save them. If you open the terminal, you will see a message that tells you that a change was detected and the site was rebuilt. After that, the change should automatically appear in your browser + +Here is a walkthrough of the full process: + + + +## Use GitHub Codespaces + +What is also great about this, is that you can also use [GitHub Codespaces](https://github.com/features/codespaces) with that setup. In that case, steps 1-5 become two clicks... Here is another full walkthrough: + + diff --git a/content/docs/Contributing/InstallHugo/UsingPowershellOnWindows11/index.md b/content/docs/Contributing/InstallHugo/UsingPowershellOnWindows11/index.md new file mode 100644 index 00000000..3b64af8f --- /dev/null +++ b/content/docs/Contributing/InstallHugo/UsingPowershellOnWindows11/index.md @@ -0,0 +1,22 @@ +--- +Title: Powershell on Windows 11 +description: > + Install Hugo with a simple Powershell Script and chocolatey +--- + +It is possible to use a powershell script and Chocolatey to install and other dependencies. Execute the following script: + +```powershell +Set-ExecutionPolicy Bypass -Scope Process -Force +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 +Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +choco install -y nodejs +choco install -y hugo-extended +``` + +Once complete, in root of of the repository execute the following commands + +```powershell +git submodule update --init --recursive --depth 1 +npm install +``` diff --git a/content/docs/Contributing/InstallHugo/_index.md b/content/docs/Contributing/InstallHugo/_index.md new file mode 100644 index 00000000..30a2f7f6 --- /dev/null +++ b/content/docs/Contributing/InstallHugo/_index.md @@ -0,0 +1,9 @@ +--- +title: Install Hugo +--- + +There are multiple ways to install Hugo for you to properly preview your contributions. Please select the scenario that matches your setup. + +For the official install guide, you can visit + +{{< youtube G7umPCU-8xc >}} diff --git a/content/docs/Contributing/Templates/BestPractice/index.md b/content/docs/Contributing/Templates/BestPractice/index.md new file mode 100644 index 00000000..7a82fbe5 --- /dev/null +++ b/content/docs/Contributing/Templates/BestPractice/index.md @@ -0,0 +1,31 @@ +--- +title: "Title Here" +tags: ["AL"] +categories: ["Best Practice"] +--- + + + + +_Created by Described by _ + +## Description + +In depth description on what this Pattern is all about + +- basic components +- how the interact +- steps to implement +- considerations to take + +## Bad code + +```al +PutCodeblocksHere() +``` + +## Good code + +```al +PutCodeblocksHere() +``` diff --git a/content/docs/Contributing/Templates/Patterns/index.md b/content/docs/Contributing/Templates/Patterns/index.md new file mode 100644 index 00000000..7e39e182 --- /dev/null +++ b/content/docs/Contributing/Templates/Patterns/index.md @@ -0,0 +1,46 @@ +--- +title: "Title Here" +tags: ["AL"] +categories: ["Pattern"] +--- + + + +_Created by Described by _ + +## Abstract + +Short, descriptive and easy to remember description of this pattern. + +## Context + +Sets the stage where the pattern takes place. 1-2 sentences. + +## Problem + +What happens before this pattern is used? How can it go wrong? 1-5 lines. + +## Description + +In depth description on what this Pattern is all about + +- basic components +- how the interact +- steps to implement +- considerations to take + +## Usage + +If applicable: where is it used in an app. You can refer to any app, BaseApp, or a (public) ISV app. + +## Benefits + +1-2 sentences: what did you just solve + +## When not to use + +Usually, there are occasions where NOT to implement the pattern. List the disadvantages of this pattern here. + +## List of references + +Youtube-link? BaseApp? Tweet? ... diff --git a/content/docs/Contributing/Templates/_index.md b/content/docs/Contributing/Templates/_index.md new file mode 100644 index 00000000..55b88a6e --- /dev/null +++ b/content/docs/Contributing/Templates/_index.md @@ -0,0 +1,12 @@ +--- +title: "Templates" +--- + +We have created some template-files that you can simply copy and use. Look at them as "Patterns for describing patterns" + +We currently offer the following templates: + +- for [Patterns](/contributing/templates/patterns/) ([raw](https://raw.githubusercontent.com/microsoft/alguidelines/main/content/docs/Contributing/Templates/Patterns/index.md)) +- for [Best Practice](/contributing/templates/bestpractice/) ([raw](https://raw.githubusercontent.com/microsoft/alguidelines/main/content/docs/Contributing/Templates/BestPractice/index.md)) + +opening the "raw" link, will allow for the best copy/paste result. diff --git a/content/docs/Contributing/TheApprovalProcess/index.md b/content/docs/Contributing/TheApprovalProcess/index.md new file mode 100644 index 00000000..c95c3daf --- /dev/null +++ b/content/docs/Contributing/TheApprovalProcess/index.md @@ -0,0 +1,9 @@ ++++ +chapter = true +pre = " " +title = "Understanding the Approval Process" +weight = 100 ++++ + +(coming soon) + diff --git a/content/docs/Contributing/_index.md b/content/docs/Contributing/_index.md new file mode 100644 index 00000000..8c7a44b5 --- /dev/null +++ b/content/docs/Contributing/_index.md @@ -0,0 +1,27 @@ ++++ +chapter = true +pre = " " +title = "Contributing" +weight = 100 ++++ + +First off, we're glad you want to help! The project team is kind and helpful, so if you're not sure if you have a good first contribution, make a [Discussion](https://github.com/microsoft/alguidelines/discussions/) about it or even just create your first try. You'll get feedback and we will be happy to help refine it, if it even needs it. + +## Code of Conduct + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments. + +## Steps to Contribute + +* [Guide to Fork & PR](/docs/contributing/forkandpr/) +* Templates: + - For [patterns](/docs/contributing/templates/patterns/) + - For [guidelines](/docs/contributing/templates/guidelines/) +* [Understanding the Approval Process](/docs/contributing/theapprovalprocess/) + +Here are some [tips and tricks in terms of working with Markdown and Hugo](/docs/contributing/formattingtips/). + diff --git a/content/docs/NAVPatterns/2-anti-patterns/_index.md b/content/docs/NAVPatterns/2-anti-patterns/_index.md new file mode 100644 index 00000000..e7695291 --- /dev/null +++ b/content/docs/NAVPatterns/2-anti-patterns/_index.md @@ -0,0 +1,19 @@ ++++ +chapter = true +title = "2. Anti-Patterns" +weight = 130 +tags = ["C/AL"] ++++ +Some of the software development practices, had **not** stood the test of time. Despite that, some are still being used today by developers everywhere. + +"An **anti-pattern** (or **antipattern**) is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive" (from [Wikipedia][anchor0]) + +Since almost the beginning of the NAV Design Patterns project, we talked about documenting the anti-patterns - but never found the time. Until, spontaneously, the April 1st 2015 article had practically wrote itself in a couple of hours, with priceless contributions coming from Andreas, Elly, Nikola - and last but not least, waldo. + +Best regards, + +Bogdana Botez + + + +[anchor0]: http://en.wikipedia.org/wiki/Anti-pattern diff --git a/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/index.md b/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/index.md new file mode 100644 index 00000000..244abe96 --- /dev/null +++ b/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/index.md @@ -0,0 +1,75 @@ ++++ +title = "Nav Upgrade" +weight = 840 +tags = ["C/AL"] ++++ +## Anti-Patterns in NAV Upgrade + +_By Carlos Raul Garcia and Bogdana Botez at Microsoft Development Center Copenhagen_ + +[![ ][image0]][anchor0] + +**Context**: when NAV is upgraded, whether on-premises or in the cloud, developers have the chance to write upgrade code to move data across changing data structures. Writing good quality code will help successful upgrades. + +### General on upgrade + +**Problem:** assuming that the upgrade table contains data. + +If the table is empty, it means that either the upgrade has run, or there was no data in the original tenant; in both cases, the upgrade code should exit immediately. + +**Solution:** if using an upgrade table, always validate that the table contains data before doing anything. + +### Upgrade code, can it be rerun safely? + +**Problem**: if the upgrade code is not written in a way that makes it runnable twice ([idempotent][anchor1]), then several failures can happen, including something as critical as data corruption. + +In on premise NAV installations, if something fails at upgrade, there is no way to run only the "remaining" tasks. You will need to run the whole upgrade again, and might end-up with data that you cannot trust. + +What about the cloud? In Platform As A Service (PaaS), in some situations, the upgrade code needs to be run twice (for example, when moving tenants from a broken/frozen VM to a healthy one). + +**Solution:** Make sure each of your upgrade procedures only kicks in if it didn't run before. + +**Examples** + +The examples below have happened in real live NAV PaaS upgrade: + +**Table data overwrite** + +* **Problem**: at upgrade, a new column has bee­n added to a table and initialized with default values. In the meanwhile, during production, some of the default values are changed to production real life values. The second time the upgrade runs, those values will be overwritten with defaults, any personalization lost. +* **Solution:** before initializing with default values, check if non-default values exist. + +**Crash on math operations** + +* **Problem:** one tenant upgrade managed to divide by zero, by assuming a non-zero value. +* **Solution:** don't assume values can never be zero, always check before using them in divisions. + +**Use of external components** + +* **Problem:** a one-time registration through web services to an external service failed when attempting to register a second time. +* **Solution:** check if already registered, before attempting again. + +### Parallelism + +**Problem**: Upgrade procedures can be run in parallel, causing issues when different procedures attempt to modify the same table at the same time. + +When modifications to the same table are being made from two or more different procedures, there is no guarantee on sequential run, or on a certain order they will be run in. + +**Solution**: if sequential or ordered execution is needed, make the affected upgrade procedures local and call them all, in the desired sequence, from a public upgrade procedure. + +### Access to cloud machines + +**Problem:** dependencies on manual installation steps do not fit in the cloud. + +If Dynamics NAV is installed on-premises, then any additional setup (like dependencies of external dlls, manual configuration steps etc.) can be done manually or semi-manually by the IT admin, at first setup and upgrade. + +In the cloud, NAV partners don't have access to the machines -- hence they cannot deploy and configure those external dependencies as they did in the old on-premises installations. + +**Solution:** Don't assume you will have access to PaaS or SaaS machines. Build your solution in such a way that it doesn't depend on executing manual configurations on the host machine. + + + +[anchor0]: upgrade.png +[anchor1]: http://stackoverflow.com/questions/1077412/what-is-an-idempotent-operation#1077421 + + +[image0]: upgrade.png diff --git a/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/upgrade.png b/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/upgrade.png new file mode 100644 index 00000000..86da9c88 Binary files /dev/null and b/content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/upgrade.png differ diff --git a/content/docs/NAVPatterns/2-anti-patterns/reusable-bugs/index.md b/content/docs/NAVPatterns/2-anti-patterns/reusable-bugs/index.md new file mode 100644 index 00000000..09d31469 --- /dev/null +++ b/content/docs/NAVPatterns/2-anti-patterns/reusable-bugs/index.md @@ -0,0 +1,95 @@ ++++ +title = "Reusable Bugs" +weight = 1020 +tags = ["C/AL"] ++++ +_By Bogdana Botez, Andreas Moth, Eric Wauters (waldo), Elly Nkya, Nikola Kukrika_ + +_April 1st, 2015_ + +## Abstract + +It is difficult to come up with new and different bugs in each release, and it is a waste of resources to not use the ones which already had proven successful in the past. To avoid reinventing the wheel, we propose to come up with new innovative ways to create bugs that are reusable and generic enough to be used in various places of the application. + +## Examples + +**Option Strings** + +One example of a reusable bug is to find a heavily used table, like table 36 Sales Header, and observe one of the more important fields of type Option, for example Document Type. The OptionString property has the following value: Quote,Order,Invoice,Credit Memo,Blanket Order,Return Order, which you should update to add one option, for example Transport Order in the beginning. + +The main benefit of this reusable bug is that code changed in one place, has impact in multiple sides on the application: document creation and handling, posting etc. + +**Application Management** + +Codeunit 1, ApplicationManagement is a good place for creating reusable bugs. A simple INSERT on the RecRef on the first line of OnDatabaseInsert will create a sure and versatile bug that is reusable all over the application. Redundancy on database insertion ensures that data is surely conveyed to the data storage layer, twice. + +**God objects** + +We are considering to no longer make localization builds for countries. It consumes lab resources to keep running multiple country builds. Instead, we will merge everything into one single build. This is a bigger scale implementation of the ["God object"][anchor0]. Inside this unique build, we will use IF statements and sporadically CASE statements to select each country's behavior. For more help on how to use IFs, see also the IF .. THEN|ELSE C/AL Coding Guideline. + +**Field length economy** + +When you post a document, make sure to transfer data to a field that is smaller than the field you are assigning from. This will not immediately be caught and will only hurt a subset of the customers who uses Microsoft Dynamics NAV to its fullest. + +## Guidelines + +When bug addition is not entirely obvious, there is a second way to approach the problem. By following some general coding best practices like the ones described below, reliable bug innovation is facilitated and can come naturally as a collateral effect. + +**Code structure** + +Put everything in one function and only use comments to explain the structure of your code. And don't use functions - because this only complicates things... having to navigate from function to function, and completely lose track of where you are in the business logic. + +On top of that .. put everything in one codeunit. Because also that will simplify and make your structure more readable. + +Use Hungarian Notation on your variables, because at any time, you need to know what type, and what context your variable is on. + +Declare all your functions and variables global, so they are available at any time. + +**Don't do Unit Testing** + +Unit testing adds complexity and extra time to the stuff you're doing. Also, it eats up extra codeunits which means: it costs money. You will never be able to foresee all scenarios possible, so you're destined to forget and not test everything. So you will save time in not doing unit testing. + +**Never add images to actions** + +Because there is an image by default. When you don't provide an image on an action on a page, the application will foresee a ball... and when you never do it, your application will have a very consistent way of showing your actions, by providing that picture of that ball. On top of that, you'll save time. + +**Do not care about ControlIDs** + +When you're doing development of your product, do not care about ControlIDs, and just leave the Offset ID to the default value of 0\. This way, when merging, you will receive nice notifications, saying both you and Microsoft have added functions in that objects. You can use this feature to document all these places. + +**Hooks** + +Never apply the hook pattern. Hooks will only reduce upgrade time. This means, you will only shortly enjoy using the AMU (Application Merge Utilities). The more you change in default application, the longer it takes to upgrade, the longer you will enjoy the toolkit + +This can be taken one level higher. Simply you are not hardcore if you do not use notepad to resolve all of the merge issues. + +**How to use RecRef** + +Why fuss around declaring specific table variables, just generalize, all you need is one, two, or perhaps three RecRef variables, with a few IFs and CASEs here and there for reflection, to carry you all the way. + +**Arguments** + +Using only a few arguments on the functions is a sign of a weak developer. Stick in as many arguments as possible on the function, even if you are not using them, they could be useful in the future. + +**Just another field / action** + +Thinking of the design is overrated, each problem can be solved by adding an additional field or the table/page or with adding another action. We all know this has worked well in the past. + +**Reusability** + +We have decided that each time we fix a bug, we now also explain how it can be applied as a pattern. We then use anti-virus software to search for these patterns, to make sure we do not re-introduce these bugs anywhere else in NAV. + +**Business logic placement** + +As a best practice, we have also decided to move code into pages. Business logic should no longer be in tables and codeunits, but instead pages should know and be aware of the context and update it accordingly. As opposite to tables and codeunits, pages are aware of the context. + +## Conclusion + +Happy April Fools' Day. + +Disclaimer: this is inspired from IETF documentation published on April 1st, like for example the revolutionizing [IP over Avian Carriers][anchor1] standard. + + + +[anchor0]: http://en.wikipedia.org/wiki/God_object +[anchor1]: http://en.wikipedia.org/wiki/IP_over_Avian_Carriers diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/_index.md new file mode 100644 index 00000000..54e113cf --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/_index.md @@ -0,0 +1,28 @@ ++++ +chapter = true +title = "3. CAL Coding Guidelines" +weight = 150 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +We've decided to publish our current C/AL coding guidelines. They are actual, as per January 2015 when this is published (but might fall out of sync as time goes by). + +You can also [download the C/AL coding guidelines as pdf,][anchor0] all in one document. In contrast, on this wiki, the coding guidelines are published individually. The reason is: give you the chance to comment and share your oppinion on each one. Thanks to [waldo][anchor1] for this idea and for helping out. + +The guidelines are debatable - and it is good when they are generating debate. There is variation of opinion on the rules at Microsoft too. The plan is to simply expose what we use now. And more important, to say that guidelines could be used. Debate on individual guidelines can become heated for any programming language, but the benefit of using some guidelines stays. + +For us, those guidelines are enforced at check-in time - we are using a tool which verifies and only allows compliant check-ins. While this tool is internal and not ready to publish, we had anyways decided to open up and present the rules we use to the community, as inspiration. + +Question: Since we're having the guidelines, how come there is still C/AL code in NAV which doesn't respect them? + +Answer: all new C/AL code is bound to follow the guidelines (else it cannot be checked-in). However, the code that existed before the rules - it does not. We had done cleanup in a certain degree. Now we're gradually improving the old code base as we visit various objects in order to add new functionality, however chances are that code we didn't touch in a long time had remained in its old form. + +We're looking forward to your comments. Where you can, do provide concrete examples of the alternatives, Good and Bad. + +{{< youtube z6skKy0pkmU >}} + + + +[anchor0]: /files/CAL-Coding-Guidelines-at-Microsoft-Development-Center-Copenhagen.pdf "download the C/AL coding guidelines as pdf" +[anchor1]: /members/waldo/default.aspx "waldo" +[anchor2]: https://www.youtube.com/watch?v=z6skKy0pkmU&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=26 diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/_index.md new file mode 100644 index 00000000..f77a18f4 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/_index.md @@ -0,0 +1,11 @@ ++++ +title = "Design" +weight = 490 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +## C/AL Coding Guidelines + +## **Design** + +Find the C/AL guidelines by expanding the menu in the left. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/by-reference-parameters/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/by-reference-parameters/index.md new file mode 100644 index 00000000..c58006b1 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/by-reference-parameters/index.md @@ -0,0 +1,27 @@ ++++ +title = "By Reference Parameters" +weight = 280 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not declare parameters by reference if their values are not intended to be changed. + +Unintentional value changes might propagate. Also, it might lead people to believe that value changes are intended. + +Bad code + + LOCAL PROCEDURE ShowMessage@15(VAR Text@1000 : Text[250]); + BEGIN + Text := GetMessageText; + IF (Text <> '') AND GenJnlLineInserted THEN + MESSAGE(Text); + END; + +Good code + + LOCAL PROCEDURE ShowMessage@15(Text@1000 : Text[250]); + BEGIN + Text := GetMessageText; + IF (Text <> '') AND GenJnlLineInserted THEN + MESSAGE(Text); + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/class-coupling/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/class-coupling/index.md new file mode 100644 index 00000000..aa47a30e --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/class-coupling/index.md @@ -0,0 +1,20 @@ ++++ +title = "Class Coupling" +weight = 320 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not write functions that have high class coupling. This makes the code hard to maintain. + +Bad code + + Any procedure / trigger that has class coupling of > 30 + + + +Good code + + Any procedure / trigger that has class coupling of <= 30\. + Class coupling is computed by summing the unique instances of the following in a code block: + - every unique usage of a complex C/AL data type (table, codeunit, etc) as 1\. + - every unique usage of a DotNet type as 1\. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/cyclomatic-complexity/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/cyclomatic-complexity/index.md new file mode 100644 index 00000000..5a2aa8ac --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/cyclomatic-complexity/index.md @@ -0,0 +1,24 @@ ++++ +title = "Cyclomatic Complexity" +weight = 460 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not write functions that have high cyclomatic complexity. This makes the code hard to maintain. + +Bad code + + Any procedure / trigger that has a cyclomatic complexity > 25, using the CC3 version mentioned in [this article][anchor0]. + + + +Good code + + Any procedure / trigger that has a cyclomatic complexity <= 25, using the CC3 version. + The CC3 version is computed by summing the following in a code block: + - each IF statement as 1\. + - each entire CASE as 1\. + + + +[anchor0]: http://www.aivosto.com/project/help/pm-complexity.html diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/encapsulate-local-functionality/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/encapsulate-local-functionality/index.md new file mode 100644 index 00000000..04d84879 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/encapsulate-local-functionality/index.md @@ -0,0 +1,7 @@ ++++ +title = "Encapsulate Local Functionality" +weight = 530 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Any function used local must be defined as local. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/findset-findfirst-findlast/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/findset-findfirst-findlast/index.md new file mode 100644 index 00000000..2dc93c17 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/findset-findfirst-findlast/index.md @@ -0,0 +1,31 @@ ++++ +title = "FINDSET FINDFIRST FINDLAST" +weight = 600 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +FINDSET, FIND('+') or FIND('-') should only be used when NEXT is used and vice versa. + +Bad code + + IF Cust.FIND('-') THEN + ERROR(CustIsBlockErr) + +Good code + + IF Cust.FINDFIRST THEN + ERROR(CustIsBlockErr) + +Bad code + + IF Cust.FINDFIRST THEN + REPEAT + ... + UNTIL Cust.NEXT = 0; + +Good code + + IF Cust.FINDSET THEN + REPEAT + ... + UNTIL Cust.NEXT = 0; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/initialized-variables/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/initialized-variables/index.md new file mode 100644 index 00000000..35a05a85 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/initialized-variables/index.md @@ -0,0 +1,51 @@ ++++ +title = "Initialized Variables" +weight = 660 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Variables should always be set to a specific value, before they are used. + +Bad code + + PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39); + VAR + Pegging@1001 : Boolean; + BEGIN + IF Pegging THEN + CurrQuantity := CurrentPurchLine."Quantity (Base)" + ELSE + CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)"; + END; + +Good code + + PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39); + VAR + Pegging@1001 : Boolean; + BEGIN + Pegging := IsPegging(CurrentPurchLine); + IF Pegging THEN + CurrQuantity := CurrentPurchLine."Quantity (Base)" + ELSE + CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)"; + END; + +Bad code + + // In the example below, the function will always return FALSE. + PROCEDURE GetItemsToPlan@22() : Boolean; + BEGIN + SETRANGE("Document Type","Document Type"::Order); + ... + FINDSET + END; + +Good code + + PROCEDURE GetItemsToPlan@22() : Boolean; + BEGIN + SETRANGE("Document Type","Document Type"::Order); + ... + EXIT(FINDSET) + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/maintainability-index/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/maintainability-index/index.md new file mode 100644 index 00000000..051ac2fb --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/maintainability-index/index.md @@ -0,0 +1,43 @@ ++++ +title = "Maintainability Index" +weight = 770 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +[Maintainability Index][anchor0]: Do not write functions that have a very low maintainability index. This makes the code hard to maintain. + +Bad code + + Any procedure / trigger that has a maintainability index < 20 + + + +Good code + + Any procedure / trigger that has a maintainability index >= 20\. + The Maintainability Index is computed as a function: + - Lines Of Code (inverse proportional) + - the Halstead Volume + - Cyclomatic Complexity (inverse proportional). + +More info + +* [Halstead Volume][anchor1] +* [Cyclomatic Complexity][anchor2] + +Bad code + + Any procedure / trigger that is > 100 lines of code + + + +Good code + + Any procedure / trigger that is <= 100 lines of code. + A full C/AL Statement counts as 1 line of code + + + +[anchor0]: http://blogs.msdn.com/b/codeanalysis/archive/2007/11/20/maintainability-index-range-and-meaning.aspx +[anchor1]: http://en.wikipedia.org/wiki/Halstead_complexity_measures +[anchor2]: http://www.aivosto.com/project/help/pm-complexity.html diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/parameter-placeholders/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/parameter-placeholders/index.md new file mode 100644 index 00000000..8811e459 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/parameter-placeholders/index.md @@ -0,0 +1,35 @@ ++++ +title = "Parameter Placeholders" +weight = 920 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +The number of parameters passed to a string must match the placeholders. + +Bad code + + CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.'; + ... + ERROR(CannotDeleteLineErr,TABLECAPTION); + +Good code + + CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.'; + ... + ERROR(CannotDeleteLineErr); + + + +Bad code + + CannotUseThisFieldErr@1020 : TextConst 'ENU=You cannot use this field for %2 fields.'; + ... + ERROR(CannotUseThisFieldErr,0,Field.Class); + +Good code + + CannotUseThisFieldErr@1020 : TextConst 'ENU=You cannot use this field for %1 fields.'; + ... + ERROR(CannotUseThisFieldErr,Field.Class); + +### diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/static-object-invocation/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/static-object-invocation/index.md new file mode 100644 index 00000000..97e89c8a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/static-object-invocation/index.md @@ -0,0 +1,23 @@ ++++ +title = "Static Object Invocation" +weight = 1160 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Call objects statically whenever possible. It reduces extra noise and removes extra variables. Downside: changing the name of the object which is called statically will need a code update. + +Bad code + + LOCAL PROCEDURE Code@1(); + VAR + CAJnlPostBatch@1001 : Codeunit 1103; + BEGIN + CAJnlPostBatch.Run(CostJnlLine); + END; + +Good code + + LOCAL PROCEDURE Code@1(); + BEGIN + CODEUNIT.RUN(CODEUNIT::"CA Jnl.-Post Batch",CostJnlLine); + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unreachable-code/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unreachable-code/index.md new file mode 100644 index 00000000..69f713e1 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unreachable-code/index.md @@ -0,0 +1,27 @@ ++++ +title = "Unreachable Code" +weight = 1310 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not write code that will never be hit. + +It affects code readability and can lead to wrong assumptions. + +Bad code + + IF Type <> Type::FIELD THEN BEGIN + ... + ERROR(...); + RecRef.CLOSE; + END; + + + +Good code + + IF Type <> Type::FIELD THEN BEGIN + ... + RecRef.CLOSE; + ERROR(...); + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-initialized-variables/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-initialized-variables/index.md new file mode 100644 index 00000000..7e8f6738 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-initialized-variables/index.md @@ -0,0 +1,40 @@ ++++ +title = "Unused Initialized Variables" +weight = 1320 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +The value assigned to a variable must be used. Else the variable is not necessary. + +Bad code + + PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]); + VAR + Vendor@1001 : Record 23; + Count@1002 : Integer; + BEGIN + Count := 0; + Vendor.SETFILTER("No.",FilterStr); + IF Vendor.FINDSET THEN + REPEAT + "User ID" := USERID; + "Vendor No." := Vendor."No."; + IF INSERT THEN + Count += 1; + UNTIL Vendor.NEXT = 0; + END; + +Good code + + PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]); + VAR + Vendor@1001 : Record 23; + BEGIN + Vendor.SETFILTER("No.",FilterStr); + IF Vendor.FINDSET THEN + REPEAT + "User ID" := USERID; + "Vendor No." := Vendor."No."; + IF INSERT THEN; + UNTIL Vendor.NEXT = 0; + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-variables/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-variables/index.md new file mode 100644 index 00000000..d7bd6da8 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/unused-variables/index.md @@ -0,0 +1,46 @@ ++++ +title = "Unused Variables" +weight = 1330 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not declare variables that are unused. + +Unused variables affect readability. + +Bad code + + PROCEDURE CheckPostingDate@23(CaptionEntryNo@1005 : Text[50]); + BEGIN + IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN + ERROR(DateNotAllowedErr,Caption,EntryNo) + IF PostingDate > MaxPostingDate THEN + MaxPostingDate := PostingDate; + END + +Good code + + PROCEDURE CheckPostingDate@23(); + BEGIN + IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN + ERROR(DateNotAllowedErr,Caption,EntryNo); + IF PostingDate > MaxPostingDate THEN + MaxPostingDate := PostingDate; + END; + +Bad code + + PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal; + VAR + ItemEntry@1000 : Record 32; + Quantity@1003 : Integer; + BEGIN + EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE)); + END; + +Good code + + PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal; + BEGIN + EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE)); + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/variable-capacity-mismatch/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/variable-capacity-mismatch/index.md new file mode 100644 index 00000000..0f3d5aab --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/variable-capacity-mismatch/index.md @@ -0,0 +1,61 @@ ++++ +title = "Variable Capacity Mismatch" +weight = 1410 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not assign a value to a variable whose capacity is smaller. + +It will throw an error at runtime. + +Bad code + + FileName@1010 : Text[250]; + ... + UploadedFileName@1016 : Text[1024]; + ... + FileName := UploadedFileName; + +Good code + + FileName@1010 : Text[1024]; + ... + UploadedFileName@1016 : Text[1024]; + ... + FileName := UploadedFileName; + +Bad code + + FileName@1010 : Text[250]; + ... + UploadedFileName@1016 : Text[1024]; + ... + FileName := UploadedFileName; + +Good code + + FileName@1010 : Text[250]; + ... + UploadedFileName@1016 : Text[1024]; + ... + FileName := COPYSTR(UploadedFileName,1,250); // In case only the first 250 chars are needed. Similar for fields + +Bad code + + VAR + ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests' + Subject@1002 : Text[50]; + ... + BEGIN + ... + Subject := ExceededNumberTxt; + +Good code + + VAR + ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests' + Subject@1002 : Text[100]; + ... + BEGIN + ... + Subject := ExceededNumberTxt'; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/design/with-scope-name-collision/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/with-scope-name-collision/index.md new file mode 100644 index 00000000..d14ab704 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/design/with-scope-name-collision/index.md @@ -0,0 +1,32 @@ ++++ +title = "WITH Scope Name Collision" +weight = 1450 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Do not use the WITH scope when it has a variable whose name is the same as a local variable. This can lead to wrong code assumptions. + +**Given that** +"Contract Type" is a field on table ServiceContractHeader, then in the following example there is a parameter name clash with the field name. Which one will be used? + +Bad code + + PROCEDURE InsertData@1("Contract Type"@1000 : Option...); + ... + BEGIN + ... + WITH ServiceContractHeader DO BEGIN + ... + DimMgt.InsertServContractDim(...,"Contract Type","Contract No.",0,...); + END; + +Good code + + PROCEDURE InsertData@1(ContractType@1000 : Option...); + ... + BEGIN + ... + WITH ServiceContractHeader DO BEGIN + ... + DimMgt.InsertServContractDim(...,ContractType,"Contract No.",0,...); + END; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/internally-used-dot-net-types/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/internally-used-dot-net-types/index.md new file mode 100644 index 00000000..cb1f1493 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/internally-used-dot-net-types/index.md @@ -0,0 +1,208 @@ ++++ +title = "Internally used DotNet Types" +weight = 690 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +_(Dynamics NAV 2015)_ + + +**Dot Net Types** + +'mscorlib'.System.Convert + +'mscorlib'.System.Globalization.CultureInfo + +'mscorlib'.System.Globalization.DateTimeStyles + +'mscorlib'.System.Globalization.NumberStyles + +'mscorlib'.System.Type + +'mscorlib'.System.Array + +'mscorlib'.System.EventArgs + +'mscorlib'.System.Security.Cryptography.SHA512Managed + +'mscorlib'.System.Security.Cryptography.HashAlgorithm + +'mscorlib'.System.Text.Encoding + +'mscorlib'.System.Text.UTF8Encoding + +'mscorlib'.System.Environment + +'mscorlib'.System.IO.Directory + +'mscorlib'.System.IO.Path + +'mscorlib'.System.IO.File + +'mscorlib'.System.IO.FileAttributes + +'mscorlib'.System.Collections.ArrayList + +'mscorlib'.System.Collections.IEnumerator + +'mscorlib'.System.Collections.Generic.IEnumerator\`1 + +'mscorlib'.System.TimeSpan + +'mscorlib'.System.DateTime + +'mscorlib'.System.DateTimeKind + +'mscorlib'.System.DateTimeOffset + +'mscorlib'.System.Decimal + +'mscorlib'.System.String + +'System'.System.Diagnostics.Process + +'System'.System.Diagnostics.ProcessStartInfo + +'System'.System.IO.Compression.CompressionMode + +'System'.System.IO.Compression.GZipStream + +'System'.System.Uri + +'System'.System.UriPartial + +'System.Data'.System.Data.DataColumn + +'System.Data'.System.Data.DataTable + +'System.Data'.System.Data.DataRow + +'System.Web'.System.Web.HttpUtility + +'System.Windows.Forms'.System.Windows.Forms.DialogResult + +'System.Windows.Forms'.System.Windows.Forms.FileDialog + +'System.Windows.Forms'.System.Windows.Forms.OpenFileDialog + +'System.Windows.Forms'.System.Windows.Forms.SaveFileDialog + +'System.Windows.Forms'.System.Windows.Forms.FolderBrowserDialog + +'System.Xml'.\* + +'DocumentFormat.OpenXml'.\* + +'mscorlib'.System.IO.DirectoryInfo + +'mscorlib'.System.IO.FileInfo + +'Microsoft.Dynamics.Nav.Client.CodeViewerTypes'.Microsoft.Dynamics.Nav.Client.CodeViewerTypes.BreakpointCollection + +'Microsoft.Dynamics.Nav.Client.CodeViewerTypes'.Microsoft.Dynamics.Nav.Client.CodeViewerTypes.VariableCollection + +'Microsoft.Dynamics.Nav.SMTP'.Microsoft.Dynamics.Nav.SMTP.SmtpMessage + +'Microsoft.Dynamics.Nav.Management.DSObjectPickerWrapper'.\* + +'Microsoft.Dynamics.Nav.Timer'.\* + +'Microsoft.Dynamics.Nav.DO.ClientProxyWrapper'.\* + +'Microsoft.Dynamics.Nav.Client.BusinessChart'.\* + +'Microsoft.Dynamics.Nav.Client.BusinessChart.Model'.\* + +'Microsoft.Dynamics.Nav.Integration.Office'.\* + +'Microsoft.Dynamics.Nav.Integration.Office.Mock'.\* + +'Microsoft.Dynamics.Nav.EwsWrapper'.\* + +'Microsoft.Dynamics.Nav.EwsWrapper.ALTestHelper'.\* + +'Microsoft.Dynamics.NAV.OLSync.OLSyncSupplier'.\* + +'Microsoft.Dynamics.Nav.OLSync.Common'.\* + +'Microsoft.Dynamics.Nav.NavUserAccount'.\* + +'Microsoft.Dynamics.Nav.OpenXml'.\* + +'Microsoft.Dynamics.Nav.RapidStart'.\* + +'Microsoft.Dynamics.Framework.RapidStart.Common'.\* + +'Microsoft.Dynamics.Nav.Client.TimelineVisualization'.Microsoft.Dynamics.Nav.Client.TimelineVisualization. + +VisualizationScenarios + +'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI. + +WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionChangesRow + +'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI. + +WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionChangesDataTable + +'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI. + +WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionRow + +'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI. + +WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionDataTable + +'Microsoft.Office.Interop.Word'.\* + +'Microsoft.Office.Interop.Excel'.\* + +'Microsoft.Dynamics.BAPIWrapper'.\* + +'Microsoft.Dynamics.Nav.Types'.Microsoft.Dynamics.Nav.Types.ConfigSettings + +'Microsoft.Dynamics.Nav.DocumentService'.\* + +'Microsoft.Dynamics.Nav.DocumentService.Types'.\* + +'mscorlib'.System.IO.StreamWriter + +'Microsoft.Dynamics.Nav.Client.TimelineVisualization'.Microsoft.Dynamics.Nav.Client.TimelineVisualization. + +InteractiveTimelineVisualizationAddIn + +'System'.System.ComponentModel.CancelEventArgs + +'System'.System.Text.RegularExpressions.Regex + +'System'.System.Text.RegularExpressions.RegexOptions + +'mscorlib'.System.IO.StreamReader + +'System.Windows.Forms'.System.Windows.Forms.Control + +'System.Windows.Forms'.System.Windows.Forms.ControlEventArgs + +'System.Windows.Forms'.System.Windows.Forms.DragEventArgs + +'System.Windows.Forms'.System.Windows.Forms.GiveFeedbackEventArgs + +'System.Windows.Forms'.System.Windows.Forms.HelpEventArgs + +'System.Windows.Forms'.System.Windows.Forms.InvalidateEventArgs + +'System.Windows.Forms'.System.Windows.Forms.KeyEventArgs + +'System.Windows.Forms'.System.Windows.Forms.KeyPressEventArgs + +'System.Windows.Forms'.System.Windows.Forms.LayoutEventArgs + +'System.Windows.Forms'.System.Windows.Forms.MouseEventArgs + +'System.Windows.Forms'.System.Windows.Forms.PaintEventArgs + +'System.Windows.Forms'.System.Windows.Forms.PreviewKeyDownEventArgs + +'System.Windows.Forms'.System.Windows.Forms.QueryAccessibilityHelpEventArgs + +'System.Windows.Forms'.System.Windows.Forms.UICuesEventArgs diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/_index.md new file mode 100644 index 00000000..6edfff5a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/_index.md @@ -0,0 +1,12 @@ ++++ +title = "Internationalization" +weight = 700 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +## C/AL Coding Guidelines + +## **Internationalization** + + +Find the C/AL guidelines by expanding the menu in the left. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/using-calcdate/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/using-calcdate/index.md new file mode 100644 index 00000000..cf18657b --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/internationalization/using-calcdate/index.md @@ -0,0 +1,19 @@ ++++ +title = "Using Calcdate" +weight = 1370 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +CALCDATE should only be used with DateFormula variables. Alternatively the string should be enclosed using the <> symbols. + +Bad code + + IF ReservEntry."Expected Receipt Date" > + CALCDATE('-' + FORMAT("Dampener (Time)") + FirstDate) + THEN + +Good code + + IF ReservEntry."Expected Receipt Date" > + CALCDATE('<-' + FORMAT("Dampener (Time)") + FirstDate + '>') + THEN diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/_index.md new file mode 100644 index 00000000..714eeeb3 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/_index.md @@ -0,0 +1,11 @@ ++++ +title = "Localizability" +weight = 750 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +## C/AL Coding Guidelines + +## **Localizability** + +Find the C/AL guidelines by expanding the menu in the left. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/captionml-for-system-tables/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/captionml-for-system-tables/index.md new file mode 100644 index 00000000..4cad77f9 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/captionml-for-system-tables/index.md @@ -0,0 +1,28 @@ ++++ +title = "CaptionML on System Pages" +weight = 300 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +CaptionML should always be specified on a page field for a system table. By default, system tables do not have captions, so if you need to use them in the UI then captions need to be added. + +Bad code + + ... + { 2 ;2 ;Field ; + SourceExpr=Name } + ... + OBJECT Table 2000000000 User + ... + { 2 ; ;Name ;Text50 } + +Good code + + ... + { 2 ;2 ;Field ; + CaptionML=ENU=Name; + SourceExpr=Name } + ... + OBJECT Table 2000000000 User + ... + { 2 ; ;Name ;Text50 } diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/fieldcaption-and-tablecaption/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/fieldcaption-and-tablecaption/index.md new file mode 100644 index 00000000..ae4c8bdc --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/fieldcaption-and-tablecaption/index.md @@ -0,0 +1,24 @@ ++++ +title = "FIELDCAPTION and TABLECAPTION" +weight = 580 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +For user messages, errors etc., use FIELDCAPTION not FIELDNAME and TABLECAPTION not TABLENAME. + +Reason: + +1. The correct translation will be automatically used. +2. If the caption/name changes, then there will be a single point of change needed. + +Bad code + +```al +IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDNAME("Location Code"),...) +``` + +Good code + +```al +IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDCAPTION("Location Code"),...) +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/global-text-constants/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/global-text-constants/index.md new file mode 100644 index 00000000..0f8c2f63 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/global-text-constants/index.md @@ -0,0 +1,26 @@ ++++ +title = "Global Text Constants" +weight = 610 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Declare Text Constant as global variables. + +Bad code + +```al +PROCEDURE GetRequirementText@6(...) : Text[50]; +VAR + RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away'; +BEGIN +``` + +Good code + +```al +VAR + RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away'; +... +PROCEDURE GetRequirementText@6(...) : Text[50]; +BEGIN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/use-text-constants/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/use-text-constants/index.md new file mode 100644 index 00000000..29129263 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/use-text-constants/index.md @@ -0,0 +1,47 @@ ++++ +title = "Use Text Constants" +weight = 1360 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Pass user messages using Text Constants. It makes translation easy. + +Bad code + +```al +ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment "'; +... +IF CONFIRM(ImportAttachmentQst + Caption +'?',TRUE) THEN BEGIN +``` + +Good code + +```al +ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment %1?"'; +... +IF CONFIRM(STRSUBSTNO(ImportAttachmentQst, Caption),TRUE) THEN BEGIN +``` + +Bad code + +```al +... + IF NOT + CONFIRM( + STRSUBSTNO( + 'Difference on Periodic entries: %1 on %2' + + 'Do you want to continue?',Balance,Date), + TRUE) + THEN + ERROR('Program terminated by the user'); +``` + +Good code + +```al +DiffOnPeriodEntiesQst@100 : TextConst 'ENU="Difference on Periodic entries: %1 on %2\\ Do you want to continue?"'; +ProgramTerminatedErr@200 : TextConst 'ENU="Program terminated by the user"'; +... + IF NOT CONFIRM(STRSUBSTNO(DiffOnPeriodEntiesQst,Balance,Date),TRUE) THEN + ERROR(ProgramTerminatedErr); +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/using-optioncaptionml/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/using-optioncaptionml/index.md new file mode 100644 index 00000000..c02403b5 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/localizability/using-optioncaptionml/index.md @@ -0,0 +1,25 @@ ++++ +title = "Using OptionCaptionML" +weight = 1380 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +The OptionCaptionML should be filled in for sourceexpression using option data types. + +Bad code + + { 30 ;TextBox ;17850;0 ;150 ;423 ;Name=Selection; + SourceExpr=Selection; + DataSetFieldName=Selection } + ... + Selection@1008 : 'Open,Closed,Open and Closed'; + ... + +Good code + + { 30 ;TextBox ;17850;0 ;150 ;423 ;Name=Selection; + OptionCaptionML=ENU=Open,Closed,Open and Closed; + SourceExpr=Selection; + DataSetFieldName=Selection } + ... + Selection@1008 : 'Open,Closed,Open and Closed'; diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/_index.md new file mode 100644 index 00000000..0a930440 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/_index.md @@ -0,0 +1,13 @@ ++++ +title = "Readability" +weight = 980 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +## C/AL Coding Guidelines + +## **Readability** + +Generally, all readability rules are Microsoft style choices only. You can use them to keep consistency with the existing code. + +Find the C/AL guidelines by expanding the menu in the left. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-as-an-afterword/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-as-an-afterword/index.md new file mode 100644 index 00000000..498e7b86 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-as-an-afterword/index.md @@ -0,0 +1,24 @@ ++++ +title = "Begin as an 'After Word'" +weight = 230 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +When BEGIN follows THEN, ELSE, DO, it should be on the same line, preceded by one space character. + +Bad code + +```al +IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN + BEGIN + ... +END; +``` + +Good code + +``` +IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN BEGIN + ... +END; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-end/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-end/index.md new file mode 100644 index 00000000..a0caf6ad --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/begin-end/index.md @@ -0,0 +1,71 @@ ++++ +title = "Begin-End - Compound Only" +weight = 240 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Only use BEGIN..END to enclose compound statements. + +Bad code + +```al +IF FINDSET THEN BEGIN + REPEAT + ... + UNTIL NEXT = 0; +END; +``` + +Good code + +```al +IF FINDSET THEN + REPEAT + ... + UNTIL NEXT = 0; +``` + +Bad code + +```al +IF IsAssemblyOutputLine THEN BEGIN + TESTFIELD("Order Line No.",0); +END; +``` + +Good code + +```al +IF IsAssemblyOutputLine THEN + TESTFIELD("Order Line No.",0); +``` + +Bad code + +```al +IF FINDSET THEN + REPEAT + BEGIN + ... + END; + UNTIL NEXT = 0; +``` + +Good code + +```al +IF FINDSET THEN + REPEAT + ... + UNTIL NEXT = 0; +``` + +Exception + +```al +// Except for this case +IF X THEN BEGIN + IF Y THEN + DO SOMETHING; +END ELSE (not X) +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/binary-operator-line-start/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/binary-operator-line-start/index.md new file mode 100644 index 00000000..8ff81042 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/binary-operator-line-start/index.md @@ -0,0 +1,23 @@ ++++ +title = "Binary Operator to Start Line" +weight = 250 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not start a line with a binary operator. + +Bad code + +```al +"Quantity to Ship" := + Quantity + - "Quantity Shipped" +``` + +Good code + +```al +"Quantity to Ship" := + Quantity - + "Quantity Shipped" +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/blank-lines/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/blank-lines/index.md new file mode 100644 index 00000000..5929b91e --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/blank-lines/index.md @@ -0,0 +1,46 @@ ++++ +title = "Blank Lines" +weight = 260 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not use blank lines at the beginning or end of any functions, after BEGIN, before END, or inside multiline expressions. + +Bad code + +```al +PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer); +BEGIN + + SetupDrillDownCol(MATRIX_ColumnOrdinal); + DrillDown(FALSE,ValueType); + +END; +``` + +Good code + +```al +PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer); +BEGIN + SetupDrillDownCol(MATRIX_ColumnOrdinal); + DrillDown(FALSE,ValueType); +END; +``` + +Bad code + +```al +IF NameIsValid AND + + Name2IsValid +THEN +``` + +Good code + +```al +IF NameIsValid AND + Name2IsValid +THEN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/case-actions/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/case-actions/index.md new file mode 100644 index 00000000..d4bbea8c --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/case-actions/index.md @@ -0,0 +1,25 @@ ++++ +title = "CASE Action" +weight = 310 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +A CASE action should start on a line after the possibility. + +Bad code + +```al +CASE Letter OF + 'A': Letter2 := '10'; + 'B': Letter2 := '11'; +``` + +Good code + +```al +CASE Letter OF + 'A': + Letter2 := '10'; + 'B': + Letter2 := '11'; +``` \ No newline at end of file diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/colon-usage-in-case/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/colon-usage-in-case/index.md new file mode 100644 index 00000000..e02393fe --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/colon-usage-in-case/index.md @@ -0,0 +1,23 @@ ++++ +title = "Colon usage in CASE" +weight = 340 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The last possibility on a CASE statement must be immediately followed by a colon. + +Bad code + +```al +CASE DimOption OF + DimOption::"Global Dimension 1" : + DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code"; +``` + +Good code + +```al +CASE DimOption OF + DimOption::"Global Dimension 1": + DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code"; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-curly-brackets/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-curly-brackets/index.md new file mode 100644 index 00000000..049cbd86 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-curly-brackets/index.md @@ -0,0 +1,48 @@ ++++ +title = "Comments inside Curly Brackets" +weight = 350 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Never use curly bracket comments. During development, the "Block comment" functionality can be used instead. However, in production code, block comments are not recommended. + +Bad code + +```al +PeriodTxt: {Period} +``` + +Good code + +```al +PeriodTxt: // Period +``` + + +Bad code + +```al +PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer); +BEGIN + { + IF ShowColumnName THEN + MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Name + ELSE + MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Code; + } + MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal]; + AnalysisValue := CalcAmt(ValueType,TRUE); + MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue; +END; +``` + +Good code + +```al +PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer); +BEGIN + MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal]; + AnalysisValue := CalcAmt(ValueType,TRUE); + MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue; +END; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-spacing/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-spacing/index.md new file mode 100644 index 00000000..7203208c --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/comments-spacing/index.md @@ -0,0 +1,20 @@ ++++ +title = "Comment Spacing" +weight = 360 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Always start comments with // followed by one space character. + +Bad code + +```al +RowNo += 1000; //Move way below the budget +``` + + +Good code + +```al +RowNo += 1000; // Move way below the budget +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/end-else-pair/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/end-else-pair/index.md new file mode 100644 index 00000000..a31fb67e --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/end-else-pair/index.md @@ -0,0 +1,28 @@ ++++ +title = "END ELSE Pair" +weight = 540 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The END ELSE pair should always appear on the same line. + +Bad code + +```al +IF OppEntry.FIND('-') THEN + IF SalesCycleStage.FIND('-') THEN BEGIN + ... + END +ELSE + ... +``` + +Good code + +```al +IF OppEntry.FIND('-') THEN + IF SalesCycleStage.FIND('-') THEN BEGIN + ... +END ELSE + ... +``` \ No newline at end of file diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/indentation/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/indentation/index.md new file mode 100644 index 00000000..614aab16 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/indentation/index.md @@ -0,0 +1,114 @@ ++++ +title = "Indentation" +weight = 650 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +In general, use an indentation of two space characters. Logical expressions in the IF, WHILE, and UNTIL parts are indented at least 3, 6, and 6 spaces respectively. + +Bad code + +```al +IF GLSetup."Unrealized VAT" OR + (GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment) +``` + +Good code + +```al +IF GLSetup."Unrealized VAT" OR + (GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment) +``` + +Bad code + +```al +IF GenJnlLine."Account No." <> ICPartner.Code THEN + ICPartner.GET("Account No."); + IF GenJnlLine.Amount \> 0 THEN BEGIN + ... +``` + +Good code + +```al +IF GenJnlLine."Account No." <> ICPartner.Code THEN + ICPartner.GET("Account No."); +IF GenJnlLine.Amount > 0 THEN BEGIN + ... +``` + +Bad code + +```al +Dialog.OPEN(WindowTxt + + '@1@@@@@@@@@@@@@@@@@@@@@@@'); +``` + +Good code + +```al +Dialog.OPEN( + WindowTxt + + '@1@@@@@@@@@@@@@@@@@@@@@@@'); +``` + +Bad code + +```al +TempOldCustLedgEntry.DELETE; + // Find the next old entry for application of the new entry +``` + +Good code + +```al +TempOldCustLedgEntry.DELETE; +// Find the next old entry for application of the new entry +``` + +Bad code + +```al +IF NOT ("Applies-to Doc. Type" IN + ["Applies-to Doc. Type"::Receipt, + "Applies-to Doc. Type"::"Return Shipment"]) +``` + +Good code + +```al +IF NOT ("Applies-to Doc. Type" IN + ["Applies-to Doc. Type"::Receipt, + "Applies-to Doc. Type"::"Return Shipment"]) +``` + +Bad code + +```al +WHILE (RemAmt > 0) OR + (RemAmtLCY > 0) +DO +``` + +Good code + +```al +WHILE (RemAmt > 0) OR + (RemAmtLCY > 0) +DO +``` + +Bad code + +```al +UNTIL (RemAmt > 0) AND + (RemAmtLCY > 0); +``` + +Good code + +```al +UNTIL (RemAmt > 0) AND + (RemAmtLCY > 0) +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/keyword-pairs-indentation/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/keyword-pairs-indentation/index.md new file mode 100644 index 00000000..e109817a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/keyword-pairs-indentation/index.md @@ -0,0 +1,22 @@ ++++ +title = "Keyword Pairs - Indentation" +weight = 730 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The IF..THEN pair, WHILE..DO pair, and FOR..DO pair must appear on the same line or the same level of indentation. + +Bad code + +```al +IF (x = y) AND + (a = b) THEN +``` + +Good code + +```al +IF (x = y) AND + (a = b) +THEN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/line-start-keywords/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/line-start-keywords/index.md new file mode 100644 index 00000000..58ce7884 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/line-start-keywords/index.md @@ -0,0 +1,28 @@ ++++ +title = "Line Start Keywords" +weight = 740 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The END, IF, REPEAT, FOR, WHILE, ELSE and CASE statement should always start a line. + +Bad code + +```al +IF IsContactName THEN ValidateContactName + ELSE IF IsSalespersonCode THEN ValidateSalespersonCode + ELSE IF IsSalesCycleCode THEN ValidatSalesCycleCode; +``` + +Good code + +```al +IF IsContactName THEN + ValidateContactName +ELSE + IF IsSalespersonCode THEN + ValidateSalespersonCode + ELSE + IF IsSalesCycleCode THEN + ValidatSalesCycleCode; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/lonely-repeat/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/lonely-repeat/index.md new file mode 100644 index 00000000..a7bae1df --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/lonely-repeat/index.md @@ -0,0 +1,22 @@ ++++ +title = "Lonely Repeat" +weight = 760 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The REPEAT statement should always be alone on a line. + +Bad code + +```al +IF ReservEntry.FINDSET THEN REPEAT + +``` + + +Good code + +```al +IF ReservEntry.FINDSET THEN + REPEAT +``` \ No newline at end of file diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/named-invocations/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/named-invocations/index.md new file mode 100644 index 00000000..64b8fff3 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/named-invocations/index.md @@ -0,0 +1,20 @@ ++++ +title = "Named Invocations" +weight = 830 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +When calling an object statically use the name, not the number + +Bad code + +```al +PAGE.RUNMODAL(525,SalesShptLine) +``` + + +Good code + +```al +PAGE.RUNMODAL(PAGE::"Posted Sales Shipment Lines",SalesShptLine) +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/nested-withs/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/nested-withs/index.md new file mode 100644 index 00000000..3dcabde2 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/nested-withs/index.md @@ -0,0 +1,28 @@ ++++ +title = "Nested WITHs" +weight = 850 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not nest WITHs that reference different types of objects. + +Bad code + +```al +WITH PostedWhseShptLine DO BEGIN + ... + WITH ItemLedgEntry DO + InsertBufferRec(...,"Serial No.","Lot No.",...); + ... +END; +``` + +Good code + +```al +WITH PostedWhseShptLine DO BEGIN + ... + InsertBufferRec(...,ItemLedgEntry."Serial No.",ItemLedgEntry."Lot No.",...); + ... +END; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/one-statement-per-line/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/one-statement-per-line/index.md new file mode 100644 index 00000000..5d9e6379 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/one-statement-per-line/index.md @@ -0,0 +1,38 @@ ++++ +title = "One Statement Per Line" +weight = 910 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +A line of code should not have more than one statement. + +Bad code + +```al +IF OppEntry.FIND('-') THEN EXIT + +``` + + +Good code + +```al +IF OppEntry.FIND('-') THEN + EXIT + +``` + + +Bad code + +```al +TotalCost += Cost; TotalAmt += Amt; + +``` + +Good code + +```al +TotalCost += Cost; +TotalAmt += Amt; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/separate-if-and-else/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/separate-if-and-else/index.md new file mode 100644 index 00000000..f1c05ab4 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/separate-if-and-else/index.md @@ -0,0 +1,25 @@ ++++ +title = "Separate IF and ELSE" +weight = 1050 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +IF and ELSE statements should be on separate lines. + +Bad code + +```al +IF Atom[i+1] = '>' THEN HasLogicalOperator := TRUE ELSE BEGIN + ... +END; +``` + +Good code + +```al +IF Atom[i+1] = '>' THEN + HasLogicalOperator := TRUE +ELSE BEGIN + ... +END; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-binary-operators/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-binary-operators/index.md new file mode 100644 index 00000000..ec18a52d --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-binary-operators/index.md @@ -0,0 +1,48 @@ ++++ +title = "Spacing Binary Operators" +weight = 1120 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +There must be exactly one space character on each side of a binary operator such as = + - AND OR =. The parameter comma operator however, should have no spaces. + +Bad code + +```al +"Line Discount %" := "Line Discount Amount"/"Line Value"*100 +``` + + +Good code + +```al +"Line Discount %" := "Line Discount Amount" / "Line Value" * 100; +``` + + +Bad code + +```al +StartDate := CALCDATE('<+'+FORMAT(Days + i)+'D>', StartDate); +``` + + +Good code + +```al +StartDate := CALCDATE('<+' + FORMAT(Days + i) + 'D>',StartDate); +``` + + +Bad code + +```al +StartDate := 0D; // Initialize +``` + + +Good code + +```al +StartDate := 0D; // Initialize +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-brackets-and/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-brackets-and/index.md new file mode 100644 index 00000000..087b1ac8 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-brackets-and/index.md @@ -0,0 +1,48 @@ ++++ +title = "Spacing Brackets and ::" +weight = 1130 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +There must be no spaces characters before and after [] dimension brackets symbols or :: option symbols. + +Bad code + +```al +A[i] [j] := Amt; +``` + + +Good code + +```al +A[i][j] := Amt; +``` + + +Bad code + +```al +"Currency Exchange Rate"."Fix Exchange Rate Amount" :: Currency: +``` + + +Good code + +```al +"Currency Exchange Rate"."Fix Exchange Rate Amount"::Currency: +``` + + +Bad code + +```al +IF FIND (Which) THEN +``` + + +Good code + +```al +IF FIND(Which) THEN +``` \ No newline at end of file diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-unary-operators/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-unary-operators/index.md new file mode 100644 index 00000000..970b2d16 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/spacing-unary-operators/index.md @@ -0,0 +1,34 @@ ++++ +title = "Spacing Unary Operators" +weight = 1140 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +There must be no space between a unary operator and its argument (except for the NOT keyword). + +Bad code + +```al +IF NOT(Type = Type::Item) THEN +``` + + +Good code + +```al +IF NOT (Type = Type::Item) THEN +``` + + +Bad code + +```al +DiscAmt := - "Discount Amount"; +``` + + +Good code + +```al +DiscAmt := -"Discount Amount"; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/suggested-abbreviations/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/suggested-abbreviations/index.md new file mode 100644 index 00000000..97b84557 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/suggested-abbreviations/index.md @@ -0,0 +1,1397 @@ ++++ +title = "Suggested Abbreviations" +weight = 1170 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +### Suggested Abbreviations + +Whenever possible, do not use abbreviations in variables, functions and objects names. + +If there is no other choice, then use the suggestions below. + +**Abbreviation** + +**Text** + +Abs + +absence + +Acc + +account + +Acc + +accounting + +Accum + +accumulated + +Act + +action + +Activ + +activity + +Add + +additional + +Addr + +address + +Adj + +adjust + +Adjd + +adjusted + +Adjmt + +adjustment + +Agrmt + +agreement + +Alloc + +allocation + +Allow + +allowance + +Alt + +alternative + +Amt + +amount + +Amts + +amounts + +Ans + +answer + +Appl + +applies + +Appln + +application + +Arriv + +arrival + +Asm + +assembly + +ATO + +assemble to order + +Assgnt + +assignment + +Assoc + +associated + +Attmt + +attachment + +Auth + +authorities + +Auto + +automatic + +Avail + +availability + +Avg + +average + +BA + +ba db. + +Bal + +balance + +BOM + +bill of materials + +Blnkt + +blanket + +Budg + +budget + +Buf + +buffer + +Bus + +business + +BIM + +business interaction management + +Buy + +buying + +Calc + +calculate + +Calcd + +calculated + +Calcu + +calculation + +Cal + +calendar + +Cap + +capacity + +CRP + +capacity requirements planning + +CF + +cash flow + +CF + +cashflow + +ctlg + +catalog + +Cat + +category + +CPU + +Central Processing Unit + +Ctr + +center + +Chg + +change + +Chgs + +changes + +Char + +character + +Chars + +characters + +Chrg + +charge + +Chrgs + +charges + +Chk + +check + +Class + +classification + +coll + +collection + +col + +column + +Cmt + +comment + +Co + +company + +Comp + +component + +Cmpltn + +completion + +Comps + +components + +Compn + +composition + +Compr + +compression + +Concrnt + +concurrent + +Conf + +confidential + +Cnfrmn + +confirmation + +Confl + +conflict + +Consol + +consolidate + +Consolid + +consolidation + +Consump + +consumption + +Cont + +contact + +Cntr + +container + +Contr + +contract + +Contrd + +contracted + +Ctrl + +control + +Ctrls + +controls + +Conv + +conversion + +Cor + +correction + +Corres + +correspondence + +Corresp + +corresponding + +Cst + +cost + +COGS + +sold + +Cr + +credit + +Cumul + +cumulate + +Curr + +currency + +Crnt + +current + +Cust + +customer + +CV + +customer/vendor + +Dly + +daily + +Damp + +dampener + +DBMS + +database management system + +D + +date + +Def + +definition + +Demo + +demonstration + +Dept + +department + +DP + +department/project + +Depr + +depreciation + +Desc + +description + +Dtl + +detail + +Dtld + +detailed + +Dtls + +details + +Dev + +deviation + +Diff + +difference + +Dim + +dimension + +Dir + +direct + +Disc + +discount + +Discr + +discrete + +Distr + +distribute + +Distrd + +distributed + +Distbtr + +distributor + +Distrn + +distribution + +Doc + +document + +Dupl + +duplicate + +Entrd + +entered + +Engin + +engineering + +Exch + +exchange + +Excl + +excluding + +Exec + +execute + +Expd + +expected + +Exped + +expedited + +Exp + +expense + +Expr + +expression + +Expir + +expiration + +Ext + +extended + +Expl + +explode + +Expt + +export + +Fnl + +final + +Fin + +finance + +Fisc + +fiscal + +Fnshd + +finished + +FA + +fixed asset + +Fwd + +forward + +Frt + +freight + +Gen + +general + +GL + +general ledger + +Gr + +group + +Hdr + +header + +Hist + +history + +Hol + +holiday + +HR + +human resource + +ID + +identification + +Imp + +import + +Inbnd + +inbound + +Incl + +including + +Incld + +included + +Incmg + +incoming + +ISV + +independent software vendor + +Indust + +industry + +Info + +information + +Init + +initial + +Intra + +Intrastat + +Interact + +interaction + +Integr + +integration + +Int + +interest + +Intm + +Interim + +IP + +internal protocol + +Invt + +inventory + +Invtbl + +inventoriable + +Inv + +invoice + +Invd + +invoiced + +IT + +item tracking + +Jnl + +journal + +Lang + +language + +Ledg + +ledger + +Lvl + +level + +Ln + +line + +Lt + +list + +LCY + +local currency + +Loc + +location + +Mail + +mailing + +Maint + +maintenance + +Mgt + +management + +Man + +manual + +Mfg + +manufacturing + +Mfr + +manufacturer + +Mat + +material + +Mktg + +marketing + +Max + +maximum + +Meas + +measure + +Msg + +message + +Min + +minimum + +Misc + +miscellaneous + +Mod + +modify + +Mth + +month + +Neg + +negative + +NonInvtbl + +non-inventoriable + +Notif + +notification + +No + +number + +Nos + +numbers + +Obj + +object + +Oper + +operating + +Opp + +opportunity + +Ord + +order + +Ords + +orders + +Orig + +original + +Org + +organization + +Outbnd + +outbound + +Outg + +Outgoing + +Out + +output + +Outstd + +outstanding + +Ovhd + +overhead + +Pmt + +payment + +Pct + +percent + +Persnl + +personnel + +Phys + +physical + +Pic + +picture + +Plng + +planning + +Pstd + +posted + +Post + +posting + +Pos + +positive + +Prec + +precision + +Prepmt + +prepayment + +Prod + +product + +Prod + +production + +ProdOrd + +production order + +Proj + +project + +Prop + +property + +Prspct + +prospect + +Purch + +purchase + +Purch + +purchases + +Purchr + +purchaser + +PurchOrd + +purchase order + +Qlty + +quality + +Qty + +quantity + +Questn + +questionnaire + +Qte + +quote + +RF + +radio frequency + +Rng + +range + +Rcpt + +receipt + +Rcd + +received + +Rec + +record + +Recs + +records + +Recncl + +reconcile + +Recon + +reconciliation + +Recur + +recurring + +Ref + +reference + +Reg + +register + +Regn + +registration + +Regd + +registered + +Rel + +relation + +Rels + +relations + +Rlshp + +relationship + +Rlse + +release + +Rlsd + +released + +Rem + +remaining + +Rmdr + +reminder + +Repl + +replacement + +Rplnsh + +replenish + +Rplnsht + +replenishment + +Rpt + +report + +Rep + +represent + +Repd + +represented + +Rqst + +request + +Reqd + +required + +Reqt + +requirement + +Reqts + +requirements + +Req + +requisition + +Rsv + +reserve + +Rsvd + +reserved + +Reserv + +reservation + +Resol + +resolution + +Res + +resource + +Rsp + +response + +Resp + +responsibility + +Rtn + +retain + +Rtnd + +retained + +Ret + +return + +Rets + +returns + +Revaln + +revaluation + +Rev + +reverse + +Rvw + +review + +Rnd + +round + +Rndd + +rounded + +Rndg + +rounding + +Rte + +route + +Rtng + +routing + +Rout + +routine + +Sales + +sales & receivables + +Saf + +safety + +Sched + +schedule + +Sec + +second + +Seg + +segment + +Sel + +select + +Selctn + +selection + +Seq + +sequence + +Ser + +serial + +SN + +serial number + +Serv + +service + +Sh + +sheet + +Shpt + +shipment + +Src + +source + +Spcl + +special + +Spec + +specification + +Specs + +specifications + +Std + +standard + +SF + +frequency + +Stmt + +statement + +Stat + +statistical + +Stats + +statistics + +Stk + +stock + +SKU + +stockkeeping unit + +Stm + +stream + +SQL + +structured query language + +Subcontr + +subcontract + +Subcontrd + +subcontracted + +Subcontrg + +subcontracting + +Sub + +substitute + +Subst + +substitution + +Sug + +suggest + +Sugd + +suggested + +Sugn + +suggestion + +Sum + +summary + +Suspd + +suspended + +Sympt + +symptom + +Synch + +synchronize + +Temp + +temporary + +Tot + +total + +Transac + +transaction + +Trans + +transfer + +Transln + +translation + +Trkg + +tracking + +Tblsht + +troubleshoot + +Tblshtg + +troubleshooting + +UOM + +unit of measure + +UT + +unit test + +Unreal + +unrealized + +Unrsvd + +unreserved + +Upd + +update + +Valn + +valuation + +Val + +value + +VAT + +value added tax + +Var + +variance + +Vend + +vendor + +Whse + +warehouse + +WS + +web shop + +Wksh + +worksheet + +GL + +g/l + +Pct + +% + +Three-Tier + +3-tier + +Osynch + +Outlook Synch + +## diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/temporary-variable-naming/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/temporary-variable-naming/index.md new file mode 100644 index 00000000..bbd36a69 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/temporary-variable-naming/index.md @@ -0,0 +1,33 @@ ++++ +title = "Temporary Variable Naming" +weight = 1200 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +The name of a temporary variable must be prefixed with the word Temp and not otherwise. + +Bad code + +```al +JobWIPBuffer@1002 : TEMPORARY Record 1018; +``` + + +Good code + +```al +TempJobWIPBuffer@1002 : TEMPORARY Record 1018; +``` + +Bad code + +```al +TempJobWIPBuffer@1002 : Record 1018; +``` + + +Good code + +```al +CopyOfJobWIPBuffer@1002 : Record 1018; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/textconst-suffixes/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/textconst-suffixes/index.md new file mode 100644 index 00000000..7de92fbf --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/textconst-suffixes/index.md @@ -0,0 +1,117 @@ ++++ +title = "TextConst Suffixes" +weight = 1210 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +TextConst variable names should have a suffix (an approved three-letter suffix: Msg, Tok, Err, Qst, Lbl, Txt) describing usage. + +Bad code + +```al +CannotDeleteLine@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.'; +... +ERROR(CannotDeleteLine,TABLECAPTION); +``` + +Good code + +```al +CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.'; +... +ERROR(CannotDeleteLineErr,TABLECAPTION); +``` + +Bad code + +```al +Text000@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"'; +... +SalesLine.FIELDERROR(Type,STRSUBSTNO(Text000,...); +... +``` + +Good code + +```al +TypeHasBeenChangedErr@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"'; +... +SalesLine.FIELDERROR(Type,STRSUBSTNO(TypeHasBeenChangedErr,...); +... +``` + +Bad code + +```al +Text004@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.'; +... +Window@1007 : Dialog; +... + Window.OPEN(Text004); +``` + +Good code + +```al +IndentingMsg@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.'; +... +Window@1007 : Dialog; +... + Window.OPEN(IndentingMsg); +``` + +Bad code + +```al +Text002@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?'; +... +IF CONFIRM(Text002,TRUE,GLAcc.TABLECAPTION) THEN +``` + +Good code + +```al +OpenWhereUsedWindowQst@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?'; +... +IF CONFIRM(OpenWhereUsedWindowQst,TRUE,GLAcc.TABLECAPTION) THEN +``` + +Bad code + +```al +Selection := STRMENU(Text003,2); +... +Text003@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components'; +``` + +Good code + +```al + Selection := STRMENU(CopyFromQst,2); + ... + CopyFromQst@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components'; +``` + +Bad code + +```al +DATASET +{ +... + { 1 ;1 ;Column ;Chart_of_AccountsCaption; + SourceExpr=Chart_of_AccountsCaption } +... +Chart_of_AccountsCaption@9647 : TextConst 'ENU=Chart of Accounts'; +``` + +Good code + +```al +DATASET +{ +... + { 1 ;1 ;Column ;Chart_of_AccountsCaption; + SourceExpr=ChartOfAccountsLbl } +... +ChartOfAccountsLbl@9647 : TextConst 'ENU=Chart of Accounts'; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unary-operator-line-end/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unary-operator-line-end/index.md new file mode 100644 index 00000000..11ad406e --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unary-operator-line-end/index.md @@ -0,0 +1,21 @@ ++++ +title = "Unary Operator Line End" +weight = 1250 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not end a line with unary operator. + +Bad code + +```al +"Quantity Handled (Base)" := - + "Quantity Handled (Base)"); +``` + +Good code + +```al +"Quantity Handled (Base)" := + - "Quantity Handled (Base)"); +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-compound-parenthesis/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-compound-parenthesis/index.md new file mode 100644 index 00000000..127418c2 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-compound-parenthesis/index.md @@ -0,0 +1,34 @@ ++++ +title = "Unnecessary Compound Parenthesis" +weight = 1260 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Use parenthesis only to enclose compound expressions inside compound expressions. + +Bad code + +```al +IF ("Costing Method" = "Costing Method"::Standard) THEN +``` + + +Good code + +```al +IF "Costing Method" = "Costing Method"::Standard THEN +``` + + +Bad code + +```al +ProfitPct = -(Profit) / CostAmt * 100; +``` + + +Good code + +```al +ProfitPct = -Profit / CostAmt * 100; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-else/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-else/index.md new file mode 100644 index 00000000..f3755ab2 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-else/index.md @@ -0,0 +1,24 @@ ++++ +title = "Unnecessary ELSE" +weight = 1270 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +ELSE should not be used when the last action in the THEN part is an EXIT, BREAK, SKIP, QUIT, ERROR. + +Bad code + +```al +IF IsAdjmtBinCodeChanged THEN + ERROR(AdjmtBinCodeChangeNotAllowedErr,...) +ELSE + ERROR(BinCodeChangeNotAllowedErr,...); +``` + +Good code + +```al +IF IsAdjmtBinCodeChanged THEN + ERROR(AdjmtBinCodeChangeNotAllowedErr,...) +ERROR(BinCodeChangeNotAllowedErr,...); +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-function-parenthesis/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-function-parenthesis/index.md new file mode 100644 index 00000000..9750286a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-function-parenthesis/index.md @@ -0,0 +1,34 @@ ++++ +title = "Unnecessary Function Parenthesis" +weight = 1280 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not use parenthesis in a function call if the function does not have any parameters. + +Bad code + +```al +IF ReservMgt.IsPositive() THEN +``` + + +Good code + +```al +IF ReservMgt.IsPositive THEN +``` + + +Bad code + +```al +IF ChangeStatusForm.RUNMODAL() <> ACTION::Yes THEN +``` + + +Good code + +```al +IF ChangeStatusForm.RUNMODAL <> ACTION::Yes THEN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-separators/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-separators/index.md new file mode 100644 index 00000000..ecefcdf6 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-separators/index.md @@ -0,0 +1,20 @@ ++++ +title = "Unnecessary Separators" +weight = 1290 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +There should be no unnecessary separators. + +Bad code + +```al +IF Customer.FINDFIRST THEN;; +``` + + +Good code + +```al +IF Customer.FINDFIRST THEN; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-truefalse/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-truefalse/index.md new file mode 100644 index 00000000..181b2067 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/unnecessary-truefalse/index.md @@ -0,0 +1,34 @@ ++++ +title = "Unnecessary TRUE/FALSE" +weight = 1300 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not use TRUE or FALSE keywords unnecessarily if the expression is already an logical expression. + +Bad code + +```al +IF IsPositive() = TRUE THEN +``` + + +Good code + +```al +IF IsPositive THEN +``` + + +Bad code + +``` +IF Complete <> TRUE THEN +``` + + +Good code + +```al +IF NOT Complete THEN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-already-scoped/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-already-scoped/index.md new file mode 100644 index 00000000..f24f9358 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-already-scoped/index.md @@ -0,0 +1,41 @@ ++++ +title = "Variable Already Scoped" +weight = 1400 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Do not use scope ''.'' qualifier unnecessarily when a variable is already implicitly or explicitly scoped. It keeps the code simpler. + +Bad code + +```al +ReturnRcptHeader.SETRANGE(ReturnRcptHeader."Return Order No.","Document No."); +``` + + +Good code + +```al +ReturnRcptHeader.SETRANGE("Return Order No.","Document No."); +``` + + +Bad code + +```al +WITH ChangeLogSetupTable DO BEGIN + ... + IF ChangeLogSetupTable.DELETE THEN + ... +END; +``` + +Good code + +```al +WITH ChangeLogSetupTable DO BEGIN + ... + IF DELETE THEN + ... +END; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-naming/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-naming/index.md new file mode 100644 index 00000000..a678e0a3 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variable-naming/index.md @@ -0,0 +1,71 @@ ++++ +title = "Variable Naming" +weight = 1420 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Variables that refer to a C/AL object must contain the objects name, abbreviated where necessary. + +A variable must begin with a capital letter. + +Blanks, periods, and other characters (such as parentheses) that would make quotation marks around a variable necessary must be omitted. + +If a variable is a compound of two or more words or abbreviations, each word or abbreviation should begin with a capital letter. + +Bad code + +```al +... + WIPBuffer@1002 : Record 1018 +... +OBJECT Table Job WIP Buffer +``` + +Good code + +```al +... + JobWIPBuffer@1002 : Record 1018 +... +OBJECT Table Job WIP Buffer +``` + +Bad code + +```al +... + Postline@1004 : Codeunit 12; +... +OBJECT Codeunit Gen. Jnl.-Post Line +``` + +Good code + +```al +... + GenJnlPostLine@1004 : Codeunit 12; +... +OBJECT Codeunit Gen. Jnl.-Post Line +``` + +Bad code + +```al +LOCAL PROCEDURE HandleCustDebitCredit@17(...;"Amount (LCY)"@1001 : Decimal;...); +BEGIN + IF ((... ("Amount (LCY)" \> 0)) ...) OR + ((... ("Amount (LCY)" < 0)) ...) + THEN BEGIN + ... +``` + +Good code + +```al +LOCAL PROCEDURE HandleCustDebitCredit@17(...;AmountLCY@1001 : Decimal;...); +BEGIN + IF ((... (AmountLCY \> 0)) ...) OR + ((... (AmountLCY < 0)) ...) + THEN BEGIN + ... +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variables-declarations-order/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variables-declarations-order/index.md new file mode 100644 index 00000000..d9b7cc08 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/readability/variables-declarations-order/index.md @@ -0,0 +1,21 @@ ++++ +title = "Variables Declarations Order" +weight = 1430 +tags = ["C/AL","Readability"] +categories = ["Best Practice"] ++++ +Variables declarations should be ordered by type. In general, object and complex variable types are listed first followed by simple variables. The order should be the same as the object list in the object designer for C/AL objects. Afterwards come the complex variables like RecordRef, .NET, FieldRef etc. At the end come all the simple data types in no particular order. + +Bad code + +```al +StartingDateFilter@1002 : Text[30]; +Vend@1003 : Record 23; +``` + +Good code + +```al +Vend@1003 : Record 23; +StartingDateFilter@1002 : Text[30]; +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/_index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/_index.md new file mode 100644 index 00000000..cc4c977b --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/_index.md @@ -0,0 +1,11 @@ ++++ +title = "UX" +weight = 1390 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +## C/AL Coding Guidelines + +## **User eXperience** + +Find the C/AL guidelines by expanding the menu in the left. diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/actions-images/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/actions-images/index.md new file mode 100644 index 00000000..863d0609 --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/actions-images/index.md @@ -0,0 +1,24 @@ ++++ +title = "Actions - Images" +weight = 200 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +All actions must have an image assigned to them. + +Bad code + +```al +{ 7 ;1 ;Action ; + CaptionML=ENU=Customer - &Balance; + RunObject=Report 121 } +``` + +Good code + +```al +{ 7 ;1 ;Action ; + CaptionML=ENU=Customer - &Balance; + RunObject=Report 121 } + Image=Report } +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/confirm/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/confirm/index.md new file mode 100644 index 00000000..155b747d --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/confirm/index.md @@ -0,0 +1,23 @@ ++++ +title = "CONFIRM" +weight = 380 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Always end CONFIRM with a question mark. + +Bad code + +```al +ChangeAllOpenedEntriesQst@1000 : TextConst 'ENU=Do you want to change all open entries for every customer and vendor that are not blocked'; +... +IF CONFIRM(ChangeAllOpenedEntriesQst,TRUE) THEN +``` + +Good code + +```al +ChangeAllOpenedEntriesQst@1000 : TextConst 'ENU=Do you want to change all open entries for every customer and vendor that are not blocked?'; +... +IF CONFIRM(ChangeAllOpenedEntriesQst,TRUE) THEN +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/fielderror/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/fielderror/index.md new file mode 100644 index 00000000..1f87ff3a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/fielderror/index.md @@ -0,0 +1,23 @@ ++++ +title = "FIELDERROR" +weight = 590 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Never use FIELDERROR with a period as it is automatically inserted. + +Bad code + +```al +InvalidValue@1025 : TextConst 'ENU=is invalid.'; +... +Cust.FIELDERROR("No.",InvalidValue); +``` + +Good code + +```al +InvalidValue@1025 : TextConst 'ENU=is invalid'; +... +Cust.FIELDERROR("No.",InvalidValue); +``` diff --git a/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/message-and-error/index.md b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/message-and-error/index.md new file mode 100644 index 00000000..93bce63a --- /dev/null +++ b/content/docs/NAVPatterns/3-cal-coding-guidelines/ux/message-and-error/index.md @@ -0,0 +1,23 @@ ++++ +title = "MESSAGE and ERROR" +weight = 790 +tags = ["C/AL"] +categories = ["Best Practice"] ++++ +Always end MESSAGE or ERROR with a period. + +Bad code + +```al +CustIsBlockedErr@1025 : TextConst 'ENU=You cannot %1 this type of document when Customer %2 is blocked with type %3'; +... +ERROR(CustIsBlockedErr,...); +``` + +Good code + +```al +CustIsBlockedErr@1025 : TextConst 'ENU=You cannot %1 this type of document when Customer %2 is blocked with type %3.'; +... +ERROR(CustIsBlockedErr,...); +``` diff --git a/content/docs/NAVPatterns/4-get-involved/_index.md b/content/docs/NAVPatterns/4-get-involved/_index.md new file mode 100644 index 00000000..43d27a4a --- /dev/null +++ b/content/docs/NAVPatterns/4-get-involved/_index.md @@ -0,0 +1,43 @@ ++++ +chapter = true +title = "(OLD) Get Involved" +weight = 170 +tags = ["C/AL"] ++++ + +**Reminder, this is an ARCHIVE of the Patterns site, this information is not current.** +--- + +**Spread the info** + +NAV Design Patterns are excellent materials for training and knowledge transfer. In your company, you can help new developer get to speed with NAV by recommending them to read and then present to the team one of the most common patterns: [No. Series][anchor0], [Setup Table][anchor1] and definitely [Hooks ][anchor2]which will be a great investment in reducing your upgrade time. The more experienced developers can read directly the newer patterns, like [Surrogate Key][anchor3], [Easy Update][anchor4], [Totals on Subpages][anchor5], [Using Queries instead of nested loops][anchor6] etc. + +**Become a NAV Design Pattern author** + +You have a pattern that you have used successfully? You have ideas on new patterns? You've found some existing design patterns which are used in the product but nobody (except a few) knows how it works, but you find it worth it to explain it for the NAV C/AL developers out there? + +Send your pattern idea to [Bogdana Botez][anchor7] as a private message on the community webpage. Once your first pattern is ready, we will review it as a team, and when signed off, you get author permissions on the Wiki site and from then on, you can continue publishing and editing existing patterns. However, only publish on the Wiki materials that we had signed-off (we don't have moderation capabilities yet, so we count on each author to only make meaningful/agreed changes). + +You and your company also get credit by being mentioned on the pattern and also on our patterns authors page. + +Once you have the idea, writing it down shouldn't take long. You will be helped by adopting [the template ][anchor8]that we've used. + +**Remember the rules** + +When handling design patterns, content quality is critical. We are trying our best to only publish content that is correct, relevant and has been reviewed by multiple developers. Therefore, we review and sign-off all patterns before publishing them (except for the videos made prior to 2015). All text content found on this Wiki and on the NAV Team Blog has been through one, usually multiple iterations of review. If you find something to correct, please comment on the pattern or contact [Bogdana Botez][anchor9], and we will review and update it. + +We are working on creating a set of rules, which would help keeping the content clean and the project on the correct track. [Find the rules here][anchor10]. + + + +[anchor0]: /navpatterns/1-patterns/no-series/ "No. Series" +[anchor1]: /navpatterns/1-patterns/singleton/singleton-table/setup-table/ "Setup Table" +[anchor2]: /navpatterns/1-patterns/hooks/ "Hooks" +[anchor3]: /navpatterns/1-patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/ +[anchor4]: /navpatterns/1-patterns/easy-update-of-setup-or-supplementary-information/ +[anchor5]: /navpatterns/1-patterns/totals-and-discounts-on-subpages-sales-and-purchases/ +[anchor6]: /navpatterns/1-patterns/queries/use-queries-to-replace-nested-loops/ +[anchor7]: /members/bogdana-botez/default.aspx +[anchor8]: /navpatterns/4-get-involved/template-for-writing-nav-design-patterns/ +[anchor9]: /members/bogdana-botez/default.aspx "Bogdana Botez" +[anchor10]: /navpatterns/4-get-involved/code-of-conduct/ "Find the rules here" diff --git a/content/docs/NAVPatterns/4-get-involved/code-of-conduct/index.md b/content/docs/NAVPatterns/4-get-involved/code-of-conduct/index.md new file mode 100644 index 00000000..76927c3e --- /dev/null +++ b/content/docs/NAVPatterns/4-get-involved/code-of-conduct/index.md @@ -0,0 +1,13 @@ ++++ +title = "Code of Conduct" +weight = 330 +tags = ["C/AL"] ++++ +Find below the rules to be used when disseminating or relating to the NAV Design Patterns. + +1. Only use materials published in text on the NAV Design Patterns Wiki site. If you received from us, any unpublished materials, please observe that they are subject to change and have not been approved for external use. +2. When referencing a NAV Design Pattern, you must remember to also reference its author and company where the author is employed. You will find the author and his/her company at the beginning of each pattern, under the title. +3. When referencing a NAV Design Patterns project, you must make it clear that this is a community project, driven by Microsoft, with multiple developers involved from both Microsoft and the community. +4. When using published text content of NAV Design Patterns, do not alter the text in any way that was published on the Wiki site, unless is has been reviewed by the patterns team and signed off by someone at Microsoft in writing. +5. If you have other materials which have not received explicit signoff in writing from me, where I have specifically stated that they are valid design patterns ready for publishing, please do not name them "NAV Design Patterns" (or anything similar). You are free to use your own content, but do not associate it in any way with NAV Design Patterns unless it is signed off in writing. +6. If you do choose to use your own content, you must make it clear that it is not a NAV Design Pattern. diff --git a/content/docs/NAVPatterns/4-get-involved/patterns-authors/index.md b/content/docs/NAVPatterns/4-get-involved/patterns-authors/index.md new file mode 100644 index 00000000..104b6b21 --- /dev/null +++ b/content/docs/NAVPatterns/4-get-involved/patterns-authors/index.md @@ -0,0 +1,168 @@ ++++ +title = "Patterns Authors" +weight = 930 +tags = ["C/AL"] ++++ +This is the list of people that have been part of the NAV Design Patterns team. If you would like to join the project follow the instructions provided on [Be a NAV Pattern Author][anchor0] page. + +Project administrator: [Bogdana Botez][anchor1]. + +Authors (in alphabetical order): + +* Abshishek Ghosh, Microsoft (4 patterns) + * Using Query Objects to Detect Duplicates + * Blocked Entity + * Single-Record (Setup) Table + * Temporary Dataset Report + +* Anders Larsen, Microsoft (1 pattern) + +* Easy Update of Setup or Supplementary Information + +* Andreas Moth, Microsoft (1 pattern) + +* Anti-pattern: Reusable bugs + +* Bogdan Sturzoiu, Microsoft (4 patterns) + +* Feature Localization for Data Structures +* Copy Document +* Using C/AL Query Objects Instead of Nested Loops +* Data-Driven Blocked Entity + +* Bogdana Botez, Microsoft (18 patterns) + +* Silent File Upload and Download +* Standard Journal +* No. Series +* Data Model Proxy +* Journal Error Processing +* Journal Template-Batch-Line +* Multilanguage Application Data +* SELECT DISTINCT using Queries +* Anti-patterns: reusable bugs +* Sensitive Data Encapsulation +* Data Encryption +* Single Point of Access +* Masked Text +* SSL in NAV +* Singleton +* Singleton Codeunit +* Anti-Patterns in NAV Upgrade +* Cue table + +* Carlos Raul Garcia, Microsoft (1 pattern) + +* Anti-Patterns in NAV Upgrade + +* Ciprian Iordache, Microsoft (2 patterns) + +* Activity Log +* Totals and Discounts on Subpages (Sales and Purchases) + +* David Bastide, Microsoft (3 patterns) +* Notification Lifecycle Management pattern +* Data Migration Façade +* Extending the Role Center Headlines + +* Elly Nkya, Microsoft (2 patterns) + +* Singleton Table +* Anti-patterns: reusable bugs + +* Eric Wauters (waldo), iFacto, PRS (6 patterns) + +* Hooks +* Posting Routine - Select Behaviour +* Variant Facade +* Argument Table +* Discovery Event +* Anti-patterns: reusable bugs + +* Gary Winter, agiles (1 pattern) + * Variant Façade + +* Henrik Langbak, Kim Ginnerup, Bording Data A/S (2 patterns) + +* Currently Active Record +* Released Entity + +* Jan Hoek, IDYN (2 patterns) + +* Conditional Cascading Update +* Setup Specificity Fallback + +* Jesper Schulz, Microsoft (1 pattern) + +* Error Message Processing, part I + +* Martin Dam, Microsoft (1 pattern) + +* Multi-File Download + +* Mike Borg Cardona, Microsoft (1 pattern) + +* Creating URLs to NAV Clients + +* Mostafa Balat, Microsoft (3 patterns) + +* .NET Exception Handling +* Cached Web Service Calls +* Try Method + +* Nikola Kukrika, Microsoft (7 patterns) + +* Totals and Discounts on Subpages (Sales and Purchases) +* Create Data from Templates +* Argument Table +* Instructions in UI +* Creating Custom Charts +* Variant Façade +* Anti-patterns: reusable bugs + +* Nikolai L'Estrange, from TVision Technology Ltd. in the UK_ +_ (2 patterns) + +* Observer +* Cross Session Events + +* Raed Selim, Microsoft (1 pattern) + * Product Name + +* Soumya Dutta (2 patterns) + * In-context notifications + * Data Migration Façade + +* Søren Klemmensen, 360 Visibility, PRS (2 patterns) + +* Master Data +* Surrogate Key + +* Tim Grant, Trendmicro (1 pattern) + +* Read Once Setup Record +* Report Selector (this pattern was started by 2 workgroups by multiple authors, but finalized and corrected by Tim). + +* Xavier Garonnat, knk Ingénierie (1 pattern) + +* Document + +They have also invested their time and energy in this project: + +* Eva Dupont, Microsoft - our publisher on MSDN NAV Team Blog. +* Kurt Juvyns, Microsoft - coordinator of pattern videos + +Pattern evangelists: + +* Eric Wauters (waldo), iFacto, PRS +* Gary Winter, agiles, PRS +* Mark Brummel, Brummel Dynamics Services, PRS +* Mike Doster, Mergetool +* Søren Klemmensen, 360 Visibility, PRS + +And last but not least, we have collaborated with Plataan who hired Eric Wauters from ifacto and Mark Brummel from Brummel Dynamics Services and PRS, to publish on video some of our patterns. + + + +[anchor0]: /navpatterns/4-get-involved/ +[anchor1]: /members/bogdana-botez/default.aspx "NAV Design Patterns project administrator" diff --git a/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/PatternLogo.png b/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/PatternLogo.png new file mode 100644 index 00000000..86321ea8 Binary files /dev/null and b/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/PatternLogo.png differ diff --git a/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/index.md b/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/index.md new file mode 100644 index 00000000..a47abbd6 --- /dev/null +++ b/content/docs/NAVPatterns/4-get-involved/template-for-writing-nav-design-patterns/index.md @@ -0,0 +1,60 @@ ++++ +title = "Template for writing Nav Design Patterns" +weight = 1180 +tags = ["C/AL"] ++++ +This is a guideline, some parts are optional (if there's no content, remove the whole paragraph). + +<_Your name here in italics, plus your company name_\> + +## **<\>** + +Short, descriptive and easy to remember. + +## **Pattern Logo** + +Black & white, no text on it. + +[![ ][image0]][anchor0] + +**Context**: Sets the stage where the pattern takes place. 1-2 sentences. + +**Problem**: What happens before this pattern is used? How can it go wrong? 1-5 lines. + +**Forces:** (explain why the problem is difficult to solve; state the considerations that must be taken into account when choosing a solution to a problem) + +* <<**Force 1: **short description (What is the impact of not using this pattern? Or using only partially?) \>\> +* <<**Force 2: **short description \>\> +* ... + +**Solution:** 1-2 sentences. The full description will come below. + +<\> + +<\> + +**Usage**: <\> + +<\> + +<\> + +**Benefits:** + +* **<< Benefit 1: **solves Force 1, short description\>\> +* **<< Benefit 2: **solves Force 2, short description\>\> +* ... + +**Consequences:** + +* **<\>** +* **...** + +**List of references** + + + +[anchor0]: PatternLogo.png + + +[image0]: PatternLogo.png diff --git a/content/docs/NAVPatterns/_index.md b/content/docs/NAVPatterns/_index.md new file mode 100644 index 00000000..22ecc24f --- /dev/null +++ b/content/docs/NAVPatterns/_index.md @@ -0,0 +1,17 @@ ++++ +title = "NAV Patterns Archive" +weight = 4 +tags = ["C/AL"] ++++ + +## About the archive + +This section of the site is a careful reproduction of the content of the Original Microsoft Community NAV Design Patterns project, created with permission. + +## Reading the archive + +Bear in mind, many of the style and formatting guidelines in this section have been brought forward into: +- The automatic formatting provided by the AL Extension +- The Code Analyzers + +Additionally, a variety of topics around the Windows Client and DotNet are outdated, and should only be used for either reference or if working in older environments. \ No newline at end of file diff --git a/content/docs/NAVPatterns/patterns/_index.md b/content/docs/NAVPatterns/patterns/_index.md new file mode 100644 index 00000000..527ceb23 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/_index.md @@ -0,0 +1,12 @@ +--- +title: "1. Patterns" +weight: 110 +tags: ["C/AL"] +categories: ["Pattern"] +description: > + Patterns described to be used with Microsoft Dynamics NAV +--- + +{{% alert title="Warning" color="warning" %}} +Please note that these patterns may not be up-to-date with the patterns for AL and Business Central Development. +{{% /alert %}} diff --git a/content/docs/NAVPatterns/patterns/activity-log/Activity-Log-NAV.jpg b/content/docs/NAVPatterns/patterns/activity-log/Activity-Log-NAV.jpg new file mode 100644 index 00000000..3e79a015 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/activity-log/Activity-Log-NAV.jpg differ diff --git a/content/docs/NAVPatterns/patterns/activity-log/Activity-Log.jpg b/content/docs/NAVPatterns/patterns/activity-log/Activity-Log.jpg new file mode 100644 index 00000000..eb67c90e Binary files /dev/null and b/content/docs/NAVPatterns/patterns/activity-log/Activity-Log.jpg differ diff --git a/content/docs/NAVPatterns/patterns/activity-log/index.md b/content/docs/NAVPatterns/patterns/activity-log/index.md new file mode 100644 index 00000000..f6acd62a --- /dev/null +++ b/content/docs/NAVPatterns/patterns/activity-log/index.md @@ -0,0 +1,118 @@ ++++ +title = "Activity Logs" +weight = 210 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Ciprian Iordache at Microsoft Development Center Copenhagen_ + +## Activity Log + +**Abstract** + +The Activity Log pattern tracks execution of activities. This is a Dynamics NAV specific implementation of the [Audit Log][anchor0] pattern. + +[![ ][image0]][anchor1] + +**Problem** + +In general, integrating with external systems can be very challenging, due to the complexity of the situation -- connectivity issues, asynchronous operations, user errors, etc. These challenges require sometimes re-trying several times, polling the external system, re-send/re-get data as all these activities can succeed but can very well fail. + +Similar challenges exist in situations where a lengthy, complex task, composed of different steps is to be executed by various people in various timeframes. In case of errors (but sometimes also in case of success) there will be a need to track these activities to see what happened and the actual person which did a specific step. + +In all these cases, we need to be able to troubleshoot. + +A tracking/logging functionality could be implemented for each activity/step separately, but this would lead to code duplication and problems in maintaining the code in future. + +In NAV there is already the Change Log functionality which can record all the data changes that have been done to specific tables, specific fields. However, this functionality is not available for activities. Also, there are few places where separate logging/tracking implementations were done but the current pattern proposes an unified, central way of data recording and enables the user to track all/most of the activities. + +**Solution** + +The Activity Log pattern tracks specific outcome of the activities, in order to be able to assess what went wrong/fine or who performed a specific activity. + +Activity Log pattern + +* records the activity and its outcome (error or success messages) +* assembles all messages in one central view and presents them to the user filtered for the specific activity and ordered in reverse chronological order. + +Figure below illustrates the how the Activity Log manifests in the UI. The figure shows a part of an activity log for a posted document that was sent to the document exchange service and illustrates both successful and failed activities. + +[![ ][image1]][anchor2] + +This functionality is implemented in the following way - Activity Log table (TAB710) contains a simple function that allows you to log the result of a task or activity: + +ActivityLog.LogActivity(ContextRecordID,ActivityLog.Status::Failed,ContextDescription,ActivityDescription,ActivityMessage); + +Similar to TAB700 for Error Messaging, the Activity Log table contains a RECORDID that is a link to the parent/context entity. That permits the Activity Log to be used in a generic way, for any kind of entities (tables) and it also permits filtering the data to a specific related entity only before being presenting to the user. + +The following parameters should be provided to the function: + +* RecordID: The record/context for which the activity is logged +* Status: The task/activity outcome +* Descriptions/Messages: fields that will clearly describe the state and outcome of the task + +To show the log, add a page action, with the caption including the name " Log" and link it to the image named "Log": + +```AL +{ ;1 ;Action ; + Name=ActivityLog; + CaptionML=ENU='Activity Log'; + ToolTipML=ENU='View the status and any errors if the document was sent as an electronic document or OCR file through the document exchange service.'; + ApplicationArea=#Basic,#Suite; + Image=Log; + OnAction= + VAR + ActivityLog@1000 : Record 710; + BEGIN + ActivityLog.ShowEntries(RECORDID); + END; + } +``` + + +**NAV usages** + +In Dynamics NAV 2016, there is a new feature for sending documents in electronic format to a document exchange service. In this case, sending documents requires multiple steps as it is an asynchronous activity and as such, in order to keep track of what's happening and when the Activity Log functionality was used. That offers later the possibility to see who sent and when a document was sent, when it was dispatched, if any dispatch errors and how many tries have been made until the document was finally dispatched or rejected. + +So as usages in NAV 2016, we have the document exchange and OCR features plus the related posted documents involved in the document exchange feature. + +* COD1294.TXT +* COD1410.TXT +* PAG1270.TXT +* PAG1275.TXT +* PAG143.TXT +* PAG144.TXT +* PAG189.TXT +* TAB112.TXT +* TAB114.TXT +* TAB130.TXT + +**Ideas for improvement** + +Replace the scattered similar functionality (as mentioned above, we have several places having close functionality or similar requirements) with this new pattern. + +**Consequences** + +* Use with caution, similar to the Change Log functionality, as if the pattern will be used extensively in all the activities/operations within NAV, the table might become large containing many records and might cause some performance issues when presenting the data to the client (filtering on the specific activity). +* Do not log private or confidential information (passwords, amounts, salaries, sensitive data), unless you are ok with this data to be showed to all users (even to users which normally would not have access to this data), thus overriding the permission sets. +* Log only essential information (quality over quantity). Can the logged data be used to analyze the problem, or is it just junk data? + +**NAV Versions** + +Supported from NAV 2016 + +**Related Topics** + +Error Message Processing -- provides a similar view and uses similar concepts: has a generic implementation (uses as link the same RECORDID feature) and uses same filtering functionality when displaying the data to the user. + +Audit Log -- as mentioned in the beginning, this pattern is a NAV specific implementation of the audit log pattern. + + + +[anchor0]: http://martinfowler.com/eaaDev/AuditLog.html +[anchor1]: Activity-Log.jpg +[anchor2]: Activity-Log-NAV.jpg + + +[image0]: Activity-Log.jpg +[image1]: Activity-Log-NAV.jpg diff --git a/content/docs/NAVPatterns/patterns/argument-table/0218.Argument-Table-image.png b/content/docs/NAVPatterns/patterns/argument-table/0218.Argument-Table-image.png new file mode 100644 index 00000000..a6d14a33 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/argument-table/0218.Argument-Table-image.png differ diff --git a/content/docs/NAVPatterns/patterns/argument-table/index.md b/content/docs/NAVPatterns/patterns/argument-table/index.md new file mode 100644 index 00000000..b413e0c8 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/argument-table/index.md @@ -0,0 +1,125 @@ ++++ +title = "Argument Table" +weight = 220 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally By Nikola Kukrika and waldo_ + +### Abstract + +The Argument Table pattern is used to provide an extension point for adding new arguments without changing the signature. By grouping multiple arguments into a table the code becomes more readable (function signature and the usage of the function). + +[![ ][image0]][anchor0] + +### Problem + +In CAL overloading function signature is not supported. It is also not possible to provide default values for the function arguments. + +When an argument needs to be added to the function, the existing function needs to be extracted to a new method with an additional argument and the original function will call new method. This will cause an upgrade problem in the future, since the entire body of the method is replaced. + +Second commonly occurring problem is option duplication. In order to pass options often they are duplicated in the signature. + +The last problem that can be solved is high number of arguments. Functions with a high number of arguments are hard to understand. Having arguments grouped within the table with a meaningful name will improve readability and make code easier to understanding. + +Few examples of the bad implementations are as illustrated here: + +#### Bad example 1 + + +```AL +PROCEDURE FillInVATReturnData@1200001(VAR DeclarationID@1200000 : Code [20];VAR LineID@1200001 : Code [20];VAR PeerID@1200002 : Code [20]; VAR DocumentNo@1200003: Code[20]; VAR NumberOfCopies@1200007: Integer; VAR Uploaded@1200004 : Boolean; VAR Correction@1200005 : Boolean; VAR HasValidationErr@1200006 : Boolean); +``` + +**Call** + + +```AL +FillInVATReturnData(NoSeries, NextLineID, CustomerID, DocumentNo, SingleCopy, ???, ??, ...., ...) +``` + +In this example the code is hard to read and understand. Adding an additional argument will require refactoring of the existing function. Each time a new argument is added a new function will be created. + +#### Bad example 2 + +```AL +LOCAL PROCEDURE GetTableSyncSetupW1@3(OldTableId@1002 : Integer; VAR UpgradeTableId@1001 : Integer; VAR TableUpgradeMode@1000 : 'Check, Copy, Move, Force') : Boolean; +BEGIN + CASE OldTableId OF + DATABASE::"Sales Header": + SetTableSyncSetup(0,TableUpgradeMode::Check,UpgradeTableId,TableUpgradeMode); + DATABASE::"Posting Exch. Column Def": + SetTableSyncSetup(104025,TableUpgradeMode::Copy,UpgradeTableId,TableUpgradeMode); + DATABASE::"Payment Export Data": + SetTableSyncSetup(0,TableUpgradeMode::Force,UpgradeTableId,TableUpgradeMode); + ELSE + EXIT(FALSE); + END; + EXIT(TRUE); +END; +``` + +In this example each time a new argument is added all function calls will have to be updated. Option is duplicated in the signature, which will cause issues if a new option is defined or the existing options are renamed. + +### Solution + +By grouping the arguments within the table it is possible to add additional argument and reuse it where it is needed without changing the signature. + +Multiple parameters are grouped within the single object with a meaningful name so the code becomes more readable. + +It is possible to assign default values and to have the code validation. + +Argument table should preferably be a temporary table since the implementation is simpler. + +The examples of usages addressing problems shown above are: + +#### Good example 1 + +New table +```AL +TAB 50003 VAT Return Data +PROCEDURE FillInVATReturnData@1200001(VAR VATReturnData@1200000 : Record 50003); + +VATReturnData.INIT; +VATReturnData.NumberOfCopies := GetDefaultNumberOfCopies; +VATReturnData.Uploaded := FALSE; + +FillInVATReturnData(VATReturnData); +``` + +By introducing an argument table, code is much more readable since there is a single argument for a function. It is easy to see which arguments are passed in and which are modified in a function. + +#### Good example 2 + +Good example +```AL +PROCEDURE GetTableSyncSetupW1@3(VAR TableSynchSetup@1000 : Record 2000000135); +BEGIN + SetTableSyncSetup(DATABASE::"Sales Header",0,TableSynchSetup.Mode::Check); + SetTableSyncSetup(DATABASE::"Posting Exch. Column Def",104025,TableSynchSetup.Mode::Copy); + SetTableSyncSetup(DATABASE::"Payment Export Data",0,TableSynchSetup.Mode::Force); +END; +``` + +Option definition is not encapsulated within the table. Arguments are grouped and we can add additional arguments without the need to change the signature. + +### Downsides + +You need to create one more table + +Complex types can't be embedded as fields in tables (cannot have a record field type etc). + +### NAV Usages + +Upgrade Codeunits + +### Related Patterns + +Posting Routine, Select behavior: Setting fields on existing records in order not to change the signatures. + + + +[anchor0]: 0218.Argument-Table-image.png + + +[image0]: 0218.Argument-Table-image.png diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/2260.BlockedEntityPattern.png b/content/docs/NAVPatterns/patterns/blocked-entity/2260.BlockedEntityPattern.png new file mode 100644 index 00000000..d4b6e853 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/blocked-entity/2260.BlockedEntityPattern.png differ diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png b/content/docs/NAVPatterns/patterns/blocked-entity/3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png new file mode 100644 index 00000000..ffba43d8 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/blocked-entity/3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png differ diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png b/content/docs/NAVPatterns/patterns/blocked-entity/8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png new file mode 100644 index 00000000..817956c4 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/blocked-entity/8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png differ diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/_index.md b/content/docs/NAVPatterns/patterns/blocked-entity/_index.md new file mode 100644 index 00000000..402ba137 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/blocked-entity/_index.md @@ -0,0 +1,108 @@ ++++ +title = "Blocked Entity" +weight = 270 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Abhishek Ghosh at Microsoft Development Center Copenhagen_ + +## Abstract + +The Blocked Entity is used when it is required to stop transactions for an entity (mostly master data), temporarily or permanently. + +[![ ][image0]][anchor0] + +## Description + +To block entities through metadata, read this pattern. To do the same thing through data, read [Data Driven Blocked Entity pattern][anchor1]. + +The business entity holds a state that controls if a given transaction is allowed. The state is used by the logic controlling transactions. The change of state could either be temporary or permanent. + +An example of a temporary halt is when a retail chain selling items has received lot of complaints about an item, and the company wants to stop all transactions, both purchase and sale, with that item until the dealer has clarified the issue with his supplier and possibly received a replacement for the defective stock. Another common example is during counting the physical inventory using cycle counting where the counting is done in one section of a warehouse at a time, so that the regular operations can continue in the other parts of the warehouse. In these situations, it is necessary to block all transactions, such as picks and put-aways, for a bin while warehouse counting is in progress for that bin. + +In contrast, a permanent halt to transactions could be required when an item has become obsolete (or is about to become obsolete), and the company wants to stop further purchase or sale of the item. However, the company wants to maintain the transaction history of the item and, therefore, does not want to delete the item record. + +A simple design implementation of such requirements in Microsoft Dynamics NAV is to add a Blocked field in the entity table (and on the associated page). The implementation takes this state into the logic and checks for the value of this field in related transactions. For most simple scenarios, it is sufficient to have two states on the Blocked field, specifying whether it is allowed to perform transactions for the entity or not. + +In certain situations, however, there could be different levels of blocking. For example, the company could block all sales to a customer that has overdue payments, and the company does not want to allow transactions with this customer until the payments are received. In other situations, the customer may have raised objections about an invoice, and the company has decided not to generate new invoices for the customer until the issue has been resolved. However, the company does want to continue shipping goods to the customer so as not to impact the customer's operations. In these scenarios, it may be necessary to have multiple states on the Blocked field depending on the level of restriction that is needed. + +## Usage + +As mentioned in the previous section, there are two implementations depending on business requirements: The 2-state Boolean field for simple implementations and the multi-state option field for more complex requirements. The implementation flow is similar for both patterns, except how the validation is implemented. The following discusses the two scenarios one by one. + +### Boolean Implementation + +Add a Boolean field named Blocked in the table. + +In the relevant logic, add a condition to check the status of the Blocked flag. The cheapest way is to use a TESTFIELD: + +```AL +.TESTFIELD(Blocked,FALSE); +``` + +Alternatively, you can throw a custom error message. However, you should only do that if the default error message thrown by TESTFIELD is not sufficient. + +### Option-Field Implementation + +Add an option field named Blocked in the table. The option values will reflect the different blocked states required by the company. + +Add this field on the card page (or on the List page if the entity does not have a card). As with the Boolean implementation, the convention is to add this field in the right-hand column in the General FastTab of the card page. + +Implement a function in the table that takes the transaction context as input and evaluates the Blocked field to decide whether the transaction should be allowed or not. Optionally, the function can be responsible for notifying the user and bubble up an error message straight away. + +Note: the option field assumes that only one of the multiple options can be active at a time. In other words, the options should be mutually exclusive. + +How not to use the option field in this case: if we want to block an item from sale and/or purchase, the 4 combined options would be **Block none** | **Block Sales** | **Block Purchases** | **Block Sales and Purchases**. This doesn't scale, because if now we need to block another transaction, the number of option would grow too fast. In this situations, it is better to use two Boolean fields: **Blocked Sale**: **true|false** and **Blocked Purchase: true|false**. + +A good example of usage would be for varying the behavior depending on the chosen option, for example by displaying a different error message depending on the reason an Item is blocked. In this case we can have the item **Not Blocked** | **Blocked due to defect** | **Blocked waiting for approval**, etc. + +## NAV Specific Example + +### Boolean Implementation + +[![ ][image1]][anchor2] + +An example of the Boolean implementation on the Item card. + +In codeunit 22 -- Item Jnl.-Post Line, the following lines of code have implemented a check based on the value of the Blocked field: + +```AL +IF NOT CalledFromAdjustment THEN + Item.TESTFIELD(Blocked,FALSE); +``` +### Option-Field Implementation + +[![ ][image2]][anchor3] + +An example of the option field implementation on the Customer card. + +The CheckBlockedCustOnDocs and CheckBlockedCustOnJnls functions in the Customer table are responsible for validating the Blocked state with respect to the input document type. These functions are invoked in several areas, such as posting routines, where a status check on the Blocked field is required. This is a good practice where the Blocked implementation gets more complex, as this encourages reuse and ensures uniformity of implementation. + +## NAV Usages + +Entities where the Blocked Entity has been implemented include: + +* Item +* G/L Account +* Customer +* Vendor +* Bin + +## Related Topics + +The [Released Entity][anchor4]. + +{{% alert title="Note" %}} +There was previously a video demonstration of this pattern, but it is no longer available. +{{% /alert %}} + +[anchor0]: 2260.BlockedEntityPattern.png +[anchor1]: /navpatterns/1-patterns/blocked-entity/data-driven-blocked-entity/ +[anchor2]: 8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png +[anchor3]: 3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png +[anchor4]: /navpatterns/1-patterns/released-entity/ + + +[image0]: 2260.BlockedEntityPattern.png +[image1]: 8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png +[image2]: 3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/attention.jpg b/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/attention.jpg new file mode 100644 index 00000000..bd5f1a88 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/attention.jpg differ diff --git a/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/index.md b/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/index.md new file mode 100644 index 00000000..3e4413b3 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/blocked-entity/data-driven-blocked-entity/index.md @@ -0,0 +1,113 @@ ++++ +title = "Data Driven Blocked Entity" +weight = 470 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Written by Bogdan Andrei Sturzoiu, at Microsoft Development Center Copenhagen_ + +## Abstract + +This pattern implements a generic mechanism for dynamically restricting and allowing usage of a record by the business process administrator. + +## Problem + +A NAV record can be used in a number of functionalities across the app. There are situations, however, when the administrator wants to restrict the consumption of such a record, as well as lift the restriction when it is no longer relevant. + +For example, a new customer record should not be used for posting documents until it is approved by the relevant approver. + +We could solve this by using the [Blocked Entity pattern][anchor1], but it requires database schema changes, which have an upgrade impact. + +The blocked entity pattern involves: + +1. Adding a "blocked" status field on the record (either a Boolean or in the more advanced cases, an option field refining the usage). +2. Adding specific code for the record in every place where the restriction needs to be enforced. + +In contrast, the Data-driven Blocked Entity pattern involves adding a new record (data change) to mark the restriction, instead of adding a new field (metadata change). + +## Solution + +This pattern describes a generic mechanism of adding and lifting restrictions for any type of record. + +The restriction mechanism has the following elements: + +1. Adding a restriction record for a specific reason (e.g. the record requires approval), which will act as a surrogate key (unique identifier) for the restricted record. This can be implemented through a workflow response, or directly, by calling the Restriction Management codeunit function. +2. Lifting the restriction when it is no longer necessary. Again, this can be done using a workflow response or directly by calling the dedicated function. +3. Consuming the restriction in the places of interest for a specific purpose. This is an application feature that requires a call to the Restriction Management codeunit to check for restrictions. + +Currently, the restrictions are record-based and type-less. They act as simple tokens, and they have: + +* A reason (e.g. the record requires approval) +* A purpose (e.g. the record cannot be posted). + +You must make sure to differentiate between the reason and the purpose. That is because the restriction can only be added once per record, but consumed in multiple places. + + + +## Example + +For example, we want to restrict posting Gen. Journal Lines if a customer has not been added in Account No. field. + +For this, the following components are needed: + +1. When a Gen. Journal Line is inserted, call RestrictRecordUsage in COD1550, either directly in the trigger or using an event subscriber. +2. When you validate a Customer No. as Account no. and Customer as Account Type, lift the restrictions by calling AllowRecordUsage in COD1550\. +3. The consumption of the restriction at posting is already implemented as an event in TAB81, OnCheckGenJournalLinePostRestrictions. No further action necessary. + +## NAV Usage + +All the approval workflows include a response that restricts usage of a record, and then, at the end of an approval loop, a response that allows the usage again by lifting the restriction. See responses "Add record restriction" and "Remove record restriction" implemented in COD1521\.[ +][anchor2] + +The code behind the "Add record restriction" workflow response: + +```AL +RecRef.GETTABLE(Variant); +Workflow.GET(WorkflowStepInstance."Workflow Code"); +RecordRestrictionMgt.RestrictRecordUsage(RecRef.RECORDID,STRSUBSTNO(RestrictUsageDetailsTxt,Workflow.Code,Workflow.Description)); +``` + +The code behind the "Remove record restriction" response: + +```AL +RecRef.GETTABLE(Variant); +CASE RecRef.NUMBER OF + DATABASE::"Approval Entry": + BEGIN + RecordRestrictionMgt.AllowRecordUsage(RecRef.RECORDID); + RecRef.SETTABLE(ApprovalEntry); + RecRef.GET(ApprovalEntry."Record ID to Approve"); + AllowRecordUsage(RecRef); + END; + DATABASE::"Gen. Journal Batch": + BEGIN + RecRef.SETTABLE(GenJournalBatch); + RecordRestrictionMgt.AllowGenJournalBatchUsage(GenJournalBatch); + END + ELSE + RecordRestrictionMgt.AllowRecordUsage(RecRef.RECORDID); +END; +``` + +Notice how lifting a restriction for a Gen. Journal Batch involves lifting all the restrictions for the individual journal lines in the batch (hence the special branching of the code). + + + +## Consequences + +Currently, there can only be one restriction per record. There are no restriction types. + +In the future, a type field should be added to the restriction table, to allow adding restrictions for different purposes, and to refine their consumption. For example, a posting restriction might only be enforced for restrictions originating from approvals. + +## NAV Versions + +This pattern has been introduced in Dynamics NAV 2016\. + + + +[anchor0]: attention.jpg +[anchor1]: /navpatterns/1-patterns/blocked-entity/ +[anchor2]: https://microsoft.sharepoint.com/teams/DynamicsNAV/Wiki/Nav%20Wiki%20Documents/NAV%20App%20Patterns/NAV%20App%20Patterns%20for%20Review/Data-Driven%20Blocked%20Entity.docx#_msocom_2 + + +[image0]: attention.jpg diff --git a/content/docs/NAVPatterns/patterns/cached-web-service-calls/Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png b/content/docs/NAVPatterns/patterns/cached-web-service-calls/Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png new file mode 100644 index 00000000..44cb3432 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/cached-web-service-calls/Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png differ diff --git a/content/docs/NAVPatterns/patterns/cached-web-service-calls/index.md b/content/docs/NAVPatterns/patterns/cached-web-service-calls/index.md new file mode 100644 index 00000000..fd64729e --- /dev/null +++ b/content/docs/NAVPatterns/patterns/cached-web-service-calls/index.md @@ -0,0 +1,120 @@ ++++ +title = "Cached Web Server Calls" +weight = 290 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Mostafa Balat, Microsoft Development Center Copenhagen_ + +## Abstract + +In a service-oriented deployment, web services are used to extend NAV's functionality and reach. Depending on how volatile this data is and the corresponding usage scheme, it is expected to be up-to-date within a pre-defined period of time (e.g. once a day). + +## Description + +When NAV is integrated with external services, then the user scenarios become dependent on the data and functions offered by such services. Eventually, there are different approaches through which the external data can be retrieved, stored and used. + +* Dynamic: either exposed by the external service itself or by a separate catalog that NAV can query. + * Advantage: data is always up-to-date + * Disadvantage: it requires constant connection to the data source. +* Static: hard-coded in the database for the user to benefit from. + * Advantage: data is promptly available when needed. + * Disadvantage: if data changes at some point, it will require a maintenance effort, which exposes the business process to a risk of failure. +* Cached: offered through an external service and gets pulled according to a pre-defined refresh rate or manually. + * Advantage: data is 'up-to-date' within the rules acceptable by the business process, without extra load on the network resources or the external service. + * Disadvantage: if data changes while the auto-refresh did not happen yet, the user may not have access to the latest data; however, the user can manually force a refresh of the data, if asked to do so. + +### When to Use It + +Offer data in lookups that were cached from an external service. + +### Diagram + +[![ ][image0]][anchor0] + +## Usage + +Set up an NAV feature to consume the data from an external service. Refresh the data on a pre-defined refresh rate (e.g. once a day) or when enforced by a power user or an admin. Cache the data in a table and offer it in lookups, as applicable. + +## NAV Specific Example + +### Overview + +**PAG1259 Bank Name - Data Conv. List** offers the required functionality to refresh and display the list of bank names needed to specify which file format to convert to. The page can be accessed from **PAG1260 Bank Data Conv. Service Setup** to display all available bank names. It is also used as a lookup on **PAG370 Bank Account Card**, where it offers a filtered view of the cached bank names based on the Country/Region Code field. + +If **PAG1259 Bank Name - Data Conv. List** is being open and the cached data is 'old', it refreshes the cache. The cached data is stored in **TAB1259 Bank Data Conv. Bank**. Meanwhile, the user has the chance to refresh the data using + +### Code Sample + +```AL +OnInit=BEGIN + ShortTimeout := 5000; + LongTimeout := 30000; +END; + +OnOpenPage=VAR + BankDataConvBank@1002 : Record 1259; + ImpBankListExtDataHndl@1000 : Codeunit 1289; + CountryRegionCode@1004 : Text; + HideErrors@1003 : Boolean; +BEGIN + CountryRegionCode := IdentifyCountryRegionCode(Rec,GETFILTER("Country/Region Code")); + IF BankDataConvBank.ISEMPTY THEN BEGIN + ImpBankListExtDataHndl.GetBankListFromConversionService(HideErrors,CountryRegionCode,ShortTimeout); + EXIT; + END; + RefreshBankNamesOlderThanToday(CountryRegionCode,HideErrors,ShortTimeout); +END; + +OnAction=VAR + ImpBankListExtDataHndl@1000 : Codeunit 1289; + FilterNotUsed@1001 : Text; + ShowErrors@1003 : Boolean; +BEGIN + ShowErrors := TRUE; + ImpBankListExtDataHndl.GetBankListFromConversionService(ShowErrors,FilterNotUsed,LongTimeout); +END; + +LOCAL PROCEDURE IdentifyCountryRegionCode@1(VAR BankDataConvBank@1002 : Record 1259;Filter@1000 : Text) : Text; +VAR + CompanyInformation@1001 : Record 79; + BlankFilter@1003 : Text; +BEGIN + BlankFilter := ''''''; + + IF Filter = BlankFilter THEN BEGIN + CompanyInformation.GET; + BankDataConvBank.SETFILTER("Country/Region Code",CompanyInformation."Country/Region Code"); + EXIT(BankDataConvBank.GETFILTER("Country/Region Code")); + END; + + EXIT(Filter); +END; + +LOCAL PROCEDURE RefreshBankNamesOlderThanToday@5(CountryRegionCode@1000 : Text;ShowErrors@1002 : Boolean;Timeout@1004 : Integer); +VAR + BankDataConvBank@1001 : Record 1259; + ImpBankListExtDataHndl@1003 : Codeunit 1289; +BEGIN + IF CountryRegionCode <> '' THEN + BankDataConvBank.SETFILTER("Country/Region Code",CountryRegionCode); + BankDataConvBank.SETFILTER("Last Update Date",'<%1',TODAY); + IF BankDataConvBank.FINDFIRST THEN + ImpBankListExtDataHndl.GetBankListFromConversionService(ShowErrors,CountryRegionCode,Timeout); +END; +``` + +## NAV Usages + +Bank name lookup on the Bank Account card for dynamically identifying the format to use to generate a bank-specific payment file. + +## Ideas for Improvement + +Expose the refresh rate through a setup table to make it easily configurable without changing the code. + + + +[anchor0]: Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png + + +[image0]: Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png diff --git a/content/docs/NAVPatterns/patterns/conditional-cascading-update/3124.T18_5F00_Name_5F00_OnValidate.png b/content/docs/NAVPatterns/patterns/conditional-cascading-update/3124.T18_5F00_Name_5F00_OnValidate.png new file mode 100644 index 00000000..d6710dd7 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/conditional-cascading-update/3124.T18_5F00_Name_5F00_OnValidate.png differ diff --git a/content/docs/NAVPatterns/patterns/conditional-cascading-update/index.md b/content/docs/NAVPatterns/patterns/conditional-cascading-update/index.md new file mode 100644 index 00000000..2b29989d --- /dev/null +++ b/content/docs/NAVPatterns/patterns/conditional-cascading-update/index.md @@ -0,0 +1,40 @@ ++++ +title = "Conditional Cascading Update" +weight = 370 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Jan Hoek at IDYN_ + +## Abstract + +The Conditional Cascading Update pattern is used to intelligently populate fields whose values depend on other field values. In this pattern description, the field triggering the update will be called "source field", and the depending field will be called "target field". + +## Description + +The value of one table field sometimes depends on the value of another field, typically following an application-defined transformation (note that we're talking about transformations of field values here. This has nothing to do with e.g. form transformation), such as conversion to uppercase, removal of certain characters etc. + +If the target field is non-editable, said transformation is usually the only way for the target field to receive new values, so no irreproducible information can be lost. However, if the target field is editable, the user may have cared enough to override the default (transformed) value, in which case revalidating the source field should not blindly replace the target field's value. + +## Usage + +In the OnValidate trigger of the source field, test if the target field value is either blank, or equal to the transformed value of the source field's previous contents. If it is, populate the target field's value with the transformed source field value. If it is not, do nothing (effectively preserving the value set by the user). + +## NAV Specific Example + +In the base application, this pattern can be found in Search Name/Search Description fields, which are updated with the uppercase value from the corresponding Name/Description field when the latter is validated, only if the Search Name/Description in question is currently blank, or equal to the (uppercase equivalent) of the previous contents of the Name/Description field. + +[![ ][image0]][anchor0] + +In this particular case, the transformation between source and target fields is implicit and due to the different data types of the fields (text vs. code). Note how the field triggers of the Search Name field itself do not contain any logic linked to this pattern. + +## Consequences + +There is a case when this pattern should not be used. If the target field is non-editable, this pattern will not add any value, since there won't be any user-overridden values to protect. + + + +[anchor0]: 3124.T18_5F00_Name_5F00_OnValidate.png + + +[image0]: 3124.T18_5F00_Name_5F00_OnValidate.png diff --git a/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image006.jpg b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image006.jpg new file mode 100644 index 00000000..874cea81 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image006.jpg differ diff --git a/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image008.jpg b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image008.jpg new file mode 100644 index 00000000..6746d61f Binary files /dev/null and b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image008.jpg differ diff --git a/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image010.jpg b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image010.jpg new file mode 100644 index 00000000..7216baff Binary files /dev/null and b/content/docs/NAVPatterns/patterns/copy-document/clip_5F00_image010.jpg differ diff --git a/content/docs/NAVPatterns/patterns/copy-document/clip_image002.gif-750x0.png b/content/docs/NAVPatterns/patterns/copy-document/clip_image002.gif-750x0.png new file mode 100644 index 00000000..a447a4bf Binary files /dev/null and b/content/docs/NAVPatterns/patterns/copy-document/clip_image002.gif-750x0.png differ diff --git a/content/docs/NAVPatterns/patterns/copy-document/clip_image004.gif-750x0.png b/content/docs/NAVPatterns/patterns/copy-document/clip_image004.gif-750x0.png new file mode 100644 index 00000000..35ebe222 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/copy-document/clip_image004.gif-750x0.png differ diff --git a/content/docs/NAVPatterns/patterns/copy-document/index.md b/content/docs/NAVPatterns/patterns/copy-document/index.md new file mode 100644 index 00000000..98f1bd0b --- /dev/null +++ b/content/docs/NAVPatterns/patterns/copy-document/index.md @@ -0,0 +1,121 @@ ++++ +title = "Copy Document" +weight = 390 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By Bogdan Sturzoiu at Microsoft Development Center Copenhagen_ + +## Abstract + +The goal of the Copy Document pattern is to create a replica of an existing open or closed document (posted or not posted), by moving the lines and, optionally, the header information from the source document to a destination document. + +## Description + +Documents are widely used by most of our customers. Many times, a significant portion of these documents are similar to each other, either by sharing the same customer, vendor, type, or line structure. Being able to re-use a document as a base for creating a new one is therefore an important means of saving time. + +Other business scenarios require that a newly created document is applied to an existing document. For example, in returns management, a return order can be the reversal of an existing order and can therefore be copied from the original order. Other times, there is even a legal requirement to match the document to its source. For example, credit memos need to be applied to the originating Invoice. + +For these reasons, NAV supports the copying of documents as a method to re-use or link documents. + +The Copy Document functionality is used in the following situations: + +* The user wants to create a new open sales document (Quote, Order, Blanket Order, Invoice, Return Order, Credit Memo) based on an existing posted or non-posted sales document (Quote, Blanket Order, Order, Invoice, Return Order, Credit Memo, Posted Shipment, Posted Invoice, Posted Return Receipt, Posted Credit Memo). +* The user wants to create a new open purchase document (Quote, Order, Blanket Order, Invoice, Return Order, Credit Memo) based on an existing posted or non-posted purchase document (Quote, Blanket Order, Order, Invoice, Return Order, Credit Memo, Posted Shipment, Posted Invoice, Posted Return Receipt, Posted Credit Memo). +* The user wants to create a new production order (Simulated, Planned, Firm Planned or Released) based on an existing production order (Simulated, Planned, Firm Planned, Released or Finished). +* The user wants to create a new assembly order based on an existing assembly document (Quote, Blanket Order, Order and Posted Order). +* The user wants to create a new service contract or quote based on an existing service contract or quote. +* The user wants to create all relevant return-related documents. For example, from a sales return order, the user can recreate the involved supply chain documentation, by copying the information upwards to a purchase return order (if the items need to be returned to the vendor), purchase order (if the items need to be reordered), and sales order (if the items need to be re-sent to the customer). + +**Note** + +* Not all to and from combinations are allowed. For example, you can only copy to open document types, since the posted documents are not editable. +* The destination document needs to have the header fully created. For example, a Sales Order will need to have the Sell-To Customer No. populated. + +## Usage + +The Dynamics NAV application developer can take into account using the Copy Document design pattern when they have requirements such as: + +* To provide a quick and efficient way of moving content from a document to another. +* To allow reusing the document history as a template for new documents. +* To allow linking of documents that need to be applied to each other. + +The Copy Document pattern involves the following entities: + +1. Source document tables for document header and line. For example,Sales Header/Line. +2. Destination document tables for document header and line. +**Note:** The source document header/line and destination document header/line tables do not need to be the same. For example, you can copy a Sales Shipment Header/Lines into a Sales Header/Lines. + +3. Copy Document engine: COD6620, Copy Document Mgt. +4. Copy Document report for a specific document type. The report requires the following parameters: + * Source Document Type + * Source Document No. + * Include Header (optional) + * Recalculate Lines (optional) + +Example: REP901, Copy Assembly Document + +[![ ][image0]][anchor0] + +## Usage Sequence + +**Precondition**: The user creates a new destination document Header, filling up the required information. + +**Step 1**: The user runs the Copy Document report (element no. 4), filling up the parameters: + +* Source Document Type +* Source Document No. +* Include Header and/or Recalculate Lines (not all Copy Document reports have these). + +**Step 2**: The report copies the information in the source tables (Header and Line) into the destination tables (Header and Line). + +**Post processing**: The user performs additional editing of the destination document. + +The sequence flow of the pattern is described in the following diagram. + +[![ ][image1]][anchor1] + +Example: Copy Sales Document for Credit Memos. + +In the standard version of Microsoft Dynamics NAV, the Copy Document functionality is implemented in the Sales Credit Memo window as shown in the following section. + +**** + +**Precondition**: The user enters data in PAGE44, Sales Credit Memo. + +[![ ][image2]][anchor2] + +**Step 1**: The user runs REP292, Copy Sales Document from the Sales Credit Memo window, populating the required parameters. The Include Header and Recalculate Lines fields are selected. + +[![ ][image3]][anchor3] + +**Step 2**: The Sales Credit Memo window is populated with information from the source sales document. + +[![ ][image4]][anchor4] + +**Post processing**: The user can now do additional editing of the sales credit memo. + +# NAV Implementations + +1. Copy Sales Document (REP292) +2. Copy Purchase Document (REP492) +3. Copy Service Document (REP5979) +4. Copy Assembly Document (REP901) + +{{< youtube aTiwroXwW0 >}} + + + +[anchor0]: clip_image002.gif-750x0.png +[anchor1]: clip_image004.gif-750x0.png +[anchor2]: clip_5F00_image006.jpg +[anchor3]: clip_5F00_image008.jpg +[anchor4]: clip_5F00_image010.jpg +[anchor5]: https://www.youtube.com/watch?v=aTiwroXwW_0&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=17 + + +[image0]: clip_image002.gif-750x0.png +[image1]: clip_image004.gif-750x0.png +[image2]: clip_5F00_image006.jpg +[image3]: clip_5F00_image008.jpg +[image4]: clip_5F00_image010.jpg diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/2134.Picture6.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/2134.Picture6.png new file mode 100644 index 00000000..68676a7a Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/2134.Picture6.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/2816.Picture3.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/2816.Picture3.png new file mode 100644 index 00000000..7fbf3572 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/2816.Picture3.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/3482.Picture-5.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/3482.Picture-5.png new file mode 100644 index 00000000..1e62e630 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/3482.Picture-5.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/4118.Picture1.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/4118.Picture1.png new file mode 100644 index 00000000..02767054 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/4118.Picture1.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/4341.Picture4.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/4341.Picture4.png new file mode 100644 index 00000000..93d000e2 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/4341.Picture4.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/7271.Picture4.png b/content/docs/NAVPatterns/patterns/create-data-from-templates/7271.Picture4.png new file mode 100644 index 00000000..514963fc Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-data-from-templates/7271.Picture4.png differ diff --git a/content/docs/NAVPatterns/patterns/create-data-from-templates/index.md b/content/docs/NAVPatterns/patterns/create-data-from-templates/index.md new file mode 100644 index 00000000..d51d6c92 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/create-data-from-templates/index.md @@ -0,0 +1,211 @@ ++++ +title = "Create Data from Templates" +weight = 400 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Nikola Kukrika at Microsoft Development Center Copenhagen_ + +## Abstract + +For many records, such as Items, Customers, and Vendors, users have to enter the same sets of data again and again. This is tedious, error-prone (users forget to enter a field or they choose the wrong group), and difficult to learn for some users. + +We can group sets of data as templates to speed up and simplify the process of entering data in Microsoft Dynamics NAV. For example, the process of creating a new customer could be simplified so that users only have to enter information that is specific for every individual customer, e.g. Name and Address/Contact. + +## Description + +This pattern solves the problem of creating new records based on their type. You should use it whenever there is a large set of data that could be grouped in a meaningful way. + +In Microsoft Dynamics NAV 2013 R2, we have extended the Configuration Templates feature so that templates can be used in different languages than the language they were created in. We have also added the ability to set related templates so that related records can be inserted, such as dimensions for customers, items, and vendors. + +The pattern consists of two parts: + +1\. Using templates to create new records or applying templates to existing records + +2\. Defining and updating existing templates + +## Usage + +[![ ][image0]][anchor0] + +Using the patterns involves three steps. + +1) As a first step, we must insert a record. This can be done either through C/AL code or by letting the user create a record using the **New** action. + +2) After the record is created, we must apply the template. This is done by using the **UpdateRecord** function in the **Config. Template Management** codeunit (8612). + +**Config. Template Lines** records reference one **Config. Template Header** record (lines pattern). The lines can be of type: + +* **Field** - Stores a field value that will be applied to the record +* **Related** Template -- References a Config. Template Header record for a related template. + +The **UpdateRecord** function applies values to the record one line at the time. One of the requirements was to be possible to use configuration templates in different language/regional settings than the template was created in. + +To support this scenario, when applying the **Config. Template Line** record, **GLOBALLANGUAGE** is set to the language ID of the field. This is important because the default value is stored as text, so we need to use the same formatting that NAV was running on when the template was created. Otherwise, data types, such as Boolean, Date, etc., will raise validation errors. + +Any updates to a **Config. Template Line** record will automatically update the language ID to the current one. Since lines are applied one by one, it is supported to have lines with different language IDs belonging to the same template. + +3) After we have applied the template to the record, we can insert related templates. For example, when you insert an item, you may want to insert dimensions as well. You must implement the logic to apply or then modify the related templates, since this depends on the logic and the relationship between the records. Lines with **Type = Related Template** are used to reference related templates. + +Code example (Insert a record, apply a template, and insert the related templates): + +```al +// First insert a record Customer.INSERT(TRUE); + +// Apply a template RecRef.GETTABLE(Customer); + +ConfigTemplateMgt.UpdateRecord(ConfigTemplateHeader,RecRef); +RecRef.SETTABLE(Customer); + +// Insert Dimensions -- related templates + +MiniDimensionsTemplate.InsertDimensionsFromTemplates(ConfigTemplateHeader,Customer."No.",DATABASE::Customer); +``` + +Code to insert related templates (dimensions): + +```al +FUNCTION InsertDimensionsFromTemplates(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code[20];TableID : Integer) + + // There are multiple records (multiple dimensions per master record) + // We have to set filter + ConfigTemplateLine.SETRANGE(Type,ConfigTemplateLine.Type::"Related Template"); + ConfigTemplateLine.SETRANGE("Data Template Code",ConfigTemplateHeader.Code + IF ConfigTemplateLine.FINDSET THEN + REPEAT + ConfigTemplateHeader.GET(ConfigTemplateLine."Template Code"); + + // Ensure that the table where the template belongs to is Dimensions + // We could have other related templates + IF ConfigTemplateHeader."Table ID" = DATABASE::"Default Dimension" THEN + InsertDimensionFromTemplate(ConfigTemplateHeader,MasterRecordNo,TableID); + UNTIL ConfigTemplateLine.NEXT = 0; + +// Create a new Dimensions Record and link it to the Master Record +FUNCTION InsertDimensionFromTemplate(ConfigTemplateHeader : Record "Config. Template Header";MasterRecordNo : Code[20];TableID : Integer) + DefaultDimension.INIT; + DefaultDimension."No." := MasterRecordNo; + DefaultDimension."Table ID" := TableID; + DefaultDimension."Dimension Code" := GetDefaultDimensionCode(ConfigTemplateHeader); + DefaultDimension.INSERT; + RecRef.GETTABLE(DefaultDimension); + ConfigTemplateMgt.UpdateRecord(ConfigTemplateHeader,RecRef); + RecRef.SETTABLE(DefaultDimension); +``` + +**To surface the action in the product, you have three options:** + +1. **Recommended** - Implement a separate action called **New from Template**. +2. **Optional** - Implement the apply template function on the document itself. This is especially good in scenarios where users are allowed to change the template. +3. **Alternative** - Remove the new action by configuration or set Insert Allowed to FALSE on the list (this will block the creation of new records from the lookup). Implement an application action named **New** and tie it to your code. + +**Note:** In Microsoft Dynamics C5 2014, we chose to remove the **New** action with configuration since we wanted to promote the functionality and avoid the confusion in having too many options. However this might be difficult to maintain with a larger set of pages. + +**To view or edit templates, you have two options:** + +1. Use the **Config. Template List** table (8620) and the **Config. Template Header Card** table (8618). + +This is a generic solution that is not very usable and is error-prone (no lookups, checks for length, table relation checks, etc.) The default value is a text field of 250 characters, which might be much more than the field length, and may lead to validation errors when used. Users will most likely not be able to use this page. + +[![ ][image1]][anchor1] + +2\. Implement custom pages resembling the document. + +This is optional if you want to enable the users to create and modify templates. In C5 2014, we created temporary tables with the same fields as the main record. Based on this temporary record, we built a page that resembles a document. + +Example of the **Customer Template** page: + +[![ ][image2]][anchor2] + +The goals of this solution were: + +* To make the setup page resemble a document page so that it is easy to use with basic validation and lookups. +* To have only one place to store templates and maintain only one business logic for applying them, namely in the **Configuration Template Header** table. +* To avoid any lateral effects of doing validation on the temporary master record. Doing validation on fields, even though the record itself is temporary, could permanently modify other data in the database. For example, if you insert a new record in the **Customer** table, even in a temporary table, a Contact record is created, which will not be temporary. +* Testability: It is easy to test through RecordRef that the template table matches the main table. We can compare field lengths, data types, table relations, etc. The test is able to detect that they are out of sync, so it is easy to prevent errors. + +One example in the product is the **Mini Customer Template** table (1300). + +The table itself contains very little code. OnModify, OnInsert, and OnDelete triggers update the **Configuration Header** and **Configuration Lines** tables. The following functions in the **Config. Template Management** codeunit (8612), are used for this: + +* ConfigTemplateManagement.CreateConfigTemplateAndLines +* ConfigTemplateManagement.UpdateConfigTemplateAndLines +* ConfigTemplateManagement.DeleteRelatedTemplates + +The CreateFieldRefArray function is used as an interface function on all the temporary template tables. It builds data to be read/written to the configuration templates. + +To further enhance the usability, we have provided the following additional functionality: + +* Create a template from the existing record: The user opens an existing record and creates a template from that record. All the fields that are defined in the CreateFieldRefArray function are used to create the new template. +* Templates list: This page is used by users to select templates or create new ones. Depending on which templates they are working on, we show different template cards. + +## NAV Specific Example + +In C5 2014, this is the workflow: + +The user opens the **Customers List** window and selects **New** + +[![ ][image3]][anchor3] + +From this page, the user can view the template, edit it, or create a new one. Selecting a template will populate the customer card and open a new record. From the existing record, the user has options to save as a template or opening a list of templates to maintain available templates. Selecting a template will populate the customer card and open a new record. From the existing record, the user has options to save as a template or opening a list of templates to maintain available templates. + +[![ ][image4]][anchor4] + +From the **Customer Card Template** window, we can invoke the **Dimensions** action, through which we can define the dimensions that will be inserted together with the template: + +[![ ][image5]][anchor5] + +## NAV Usages + +This pattern is used in Microsoft Dynamics C5 2014 in the following objects: + +* Temporary template tables: + * **Mini Customer Template** table (1300) + * **Mini Item Template** table (1301) + * **Mini Dimensions Template** table(1302) + * **Mini Vendor Template** table (1303) + +* Pages to define templates: + * **Mini Customer Template Card** page (1341) + * **Mini Item Template Card** page (,1342) + * **Mini Dimensions Template List** page (1343) + * **Mini Vendor Template Card** page (1344) + +* Pages that use the templates: + * **Mini Customer List** page (1301) + * **Mini Item List** page ( 1303) + * **Mini Vendor List** page (1331) + +In the standard version of Microsoft Dynamics NAV, we use the **Apply Template** action on the following pages: + +* **Customer Card** page (21) +* **Vendor Card** page (26) +* **Item Card** page (30) +* **Resource Card** page (, 76) +* Other similar cards. + +## Ideas for improvement + +Implement the solution in the standard version of Microsoft Dynamics NAV and extend the Apply Template functionality to insert dimensions. + +It is possible that users end up with a large number of templates if they need many different data combinations. An improvement could be to split templates into smaller groups, grouping only part of the fields that are related, and then apply only these. + +{{< youtube F0CTvoyKSmI >}} + + + +[anchor0]: 4118.Picture1.png +[anchor1]: 2816.Picture3.png +[anchor2]: 7271.Picture4.png +[anchor3]: 4341.Picture4.png +[anchor4]: 3482.Picture-5.png +[anchor5]: 2134.Picture6.png +[anchor6]: https://www.youtube.com/watch?v=F0CTvoyKSmI&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=20 + + +[image0]: 4118.Picture1.png +[image1]: 2816.Picture3.png +[image2]: 7271.Picture4.png +[image3]: 4341.Picture4.png +[image4]: 3482.Picture-5.png +[image5]: 2134.Picture6.png diff --git a/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/1778.url1.jpg b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/1778.url1.jpg new file mode 100644 index 00000000..5f99e021 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/1778.url1.jpg differ diff --git a/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/7802.url2.jpg b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/7802.url2.jpg new file mode 100644 index 00000000..61d080c4 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/7802.url2.jpg differ diff --git a/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/index.md b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/index.md new file mode 100644 index 00000000..5f604ef4 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/create-urls-to-nav-clients/index.md @@ -0,0 +1,144 @@ ++++ +title = "Create URLs to NAV Clients" +weight = 410 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By Mike Borg Cardona and Bogdana Botez at Microsoft Development Center Copenhagen_ + +## Abstract + +This article illustrates NAV platform functionality to be used by C/AL developers. + +The URL builder function, GETURL, is released in Microsoft Dynamics NAV 2013 R2 to reduce coding time for developers who need to create various URL strings to run application objects in either the win client, the web client, or on web services. In addition, the GETURL function makes multitenancy features more transparent to C/AL developers. + +## Description + +Ever had to construct win client URLs like the one below? + +dynamicsnav://myserver:7046/myInstance/myCompany/runpage?page=26 + +Today, Microsoft Dynamics NAV also provides a web client. This means that you must update your code to construct web client URLs too. What about multitenancy? The URL Builder should know if it is running in a multitenant setup and it should know how to choose the right tenant. What about maintaining this code? + +The good news is that GETURL has been introduced to handle all URL building for you. + +GETURL automatically handles: + +* Multitenancy +* Correct URL format for each client +* Publicly accessible hostnames. + +## Usage + +The format is: + +[String :=] GETURL(ClientType[, Company][, Object Type][, Object Id][, Record]) + +Where: + +* **Client Type** can be: Current, Default, Windows, Web, SOAP, or OData. This enables a range of scenarios for the C/AL developer, such as moving to the web client without changing code to decide where the URL should point to. This is done either by setting Client Type to Current, and just ensuring that web is used to invoke the link creation, or by setting Client Type to Default and changing its value to Web when it is ready to move to the web platform. +* **Object Type** and **Object ID** define the type of the application object to run (Table, Page, Report, Codeunit, Query, or XMLport) and its ID. +* **Record** specifies the actual data to run the URL on, such as: + +```al +Vendor.GET("Account No."); + +GETURL(CLIENTTYPE:WEB,COMPANYNAME, OBJECTTYPE::Page,27,Vendor) +``` + +**Note**: It is currently not possible to set filters on the record that you sent as a last parameter to the GETURL function. However, it is possible to write your own code to compute and append the filter string to the URL that is created by the GETURL function. + +The server name and instance are extracted automatically by GETURL and do not need to be specified by the C/AL developer. Furthermore, the multitenancy setup is transparent to the C/AL developer. No multitenancy parameters are specified when you call GETURL, because the function knows from the server setup if it is running in a multitenant environment and if so, it will add a string like "&tenant=MyTenant" to the URL. + +## When to Use + +The GETURL function can generally be used every time a URL must be created. The following are some scenarios where the function is particularly useful. + +* Document approvals. For more information, see the "NAV Usage Example" section. +* Reports containing drill-down links. (Beware of the resource cost of adding a new URL element to the Report dataset.) +* When planning to write code for, or migrate to, various display targets (Microsoft Dynamics NAV Windows client, Microsoft Dynamics NAV web client, Microsoft Dynamics NAV web services) without having to explicitly specify which client to use. + +## Examples of Usage + +The following are examples of calls to GETURL and their corresponding return value: + +Command | URL +--------|------ +GETURL(CLIENTTYPE::Win) | dynamicsnav://MyServer:7046/DynamicsNAV71// +GETURL(CLIENTTYPE::Web) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient +GETURL(CLIENTTYPE::OData) | http://MyServer:7048/DynamicsNAV71/OData +GETURL(CLIENTTYPE::SOAP) | http://MyServer:7047/DynamicsNAV71/WS/Services +GETURL(CLIENTTYPE::Current) ie. When running this code on a Win client session | dynamicsnav://MyServer:7046/DynamicsNAV71// +GETURL(CLIENTTYPE::Default) ie. When the Server config key DefaultClient is set to Windows | dynamicsnav://MyServer:7046/DynamicsNAV71// +GETURL(CLIENTTYPE::Windows,COMPANYNAME) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/ +GETURL(CLIENTTYPE::Windows,'') | dynamicsnav://MyServer:7046/DynamicsNAV71// +GETURL(CLIENTTYPE::Windows,'NONEXISTING Corp') | dynamicsnav://MyServer:7046/DynamicsNAV71/NONEXISTING Corp/ +GETURL(CLIENTTYPE::Web,COMPANYNAME) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS +GETURL(CLIENTTYPE::Web,'') | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient +GETURL(CLIENTTYPE::Web,'NONEXISTING Corp') | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=NONEXISTING Corp +GETURL(CLIENTTYPE::OData,COMPANYNAME) | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS') +GETURL(CLIENTTYPE::OData,'') | http://MyServer:7048/DynamicsNAV71/OData +GETURL(CLIENTTYPE::OData,'NONEXISTING Corp') | http://MyServer:7048/DynamicsNAV71/OData/Company('NONEXISTING Corp') +GETURL(CLIENTTYPE::SOAP,COMPANYNAME) | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Services +GETURL(CLIENTTYPE::SOAP,'') | http://MyServer:7047/DynamicsNAV71/WS/Services +GETURL(CLIENTTYPE::SOAP,'NONEXISTING Corp') | http://MyServer:7047/DynamicsNAV71/WS/NONEXISTING Corp/Services +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Table,27) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runtable?table=27 +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27 +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Report,6) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runreport?report=6 +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runcodeunit?codeunit=5065 +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Query,9150) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runquery?query=9150 +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runxmlport?xmlport=5150 +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27) ie. When the Web Service is published | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150) ie. When the Web Service is published | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/QUE9150MyCustomers +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27)Â ie. When the Web Service is published | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Page/PAG27Vendors +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065) ie. When the Web Service is published | http://MyServer:7047/DynamicsNAV71/WS/CRONUS/Codeunit/COD5065EmailLogging +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw +GETURL(CLIENTTYPE::Windows,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page | dynamicsnav://MyServer:7046/DynamicsNAV71/CRONUS/runpage?page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27,record) List Page | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=27&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,26,record) Card Page | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=26&bookmark=23;FwAAAAJ7/0kAQwAxADAAMwAw +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27,record) | http://MyServer:7048/DynamicsNAV71/OData/Company('CRONUS')/PAG27Vendors('IC1030') +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Page,27) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&page=27 +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Report,6) | https://navwebsrvr:443/DynamicsNAV71_Instance1/Webclient?company=CRONUS&report=6 + +If the GETURL function is called with invalid parameters, it will return an empty string. In that case, you can find the related error text by calling the GETLASTERRORTEXT function. + +Function Call | Error Message +--------|------ +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::Query,9150) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::Web,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Page,27) | The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Report,6) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::Query,9150) | The Query object, 9150, that is specified for the GetUrl function has not been published in the Web Services table. +GETURL(CLIENTTYPE::OData,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Table,27) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27) | The Page object, 27, that is specified for the GetUrl function has not been published in the Web Services table. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Report,6) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Codeunit,5065) | The Codeunit object, 5065, that is specified for the GetUrl function has not been published in the Web Services table. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Query,9150) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::XmlPort,5150) | The specified object type parameter for the GetUrl function is not valid. +GETURL(CLIENTTYPE::SOAP,COMPANYNAME,OBJECTTYPE::Page,27,record) | You cannot specify a record parameter for the GetUrl function when the object type is SOAP + +## NAV Specific Example + +The following example shows how to use the GETURL function in codeunit 440 to ensure that the notification mail in Document Approvals can link to both the Microsoft Dynamics NAV Windows client and the Microsoft Dynamics NAV web client: + +[![ ][image0]][anchor0] + +This resulting UI looks as follows. + +[![ ][image1]][anchor1] + +The first link opens the approval document in the Microsoft Dynamics NAV Windows client. The second link (Web view) opens the same document in the Microsoft Dynamics NAV web client. + + + +[anchor0]: 1778.url1.jpg +[anchor1]: 7802.url2.jpg + + +[image0]: 1778.url1.jpg +[image1]: 7802.url2.jpg diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/0246.Picture6.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/0246.Picture6.png new file mode 100644 index 00000000..8a71988e Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/0246.Picture6.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/1411.Picture2.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/1411.Picture2.png new file mode 100644 index 00000000..c93679c0 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/1411.Picture2.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/1541.Picture7.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/1541.Picture7.png new file mode 100644 index 00000000..b05555d8 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/1541.Picture7.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/1781.Picture5.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/1781.Picture5.png new file mode 100644 index 00000000..636e0240 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/1781.Picture5.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/1803.Picture7.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/1803.Picture7.png new file mode 100644 index 00000000..d6b26d7c Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/1803.Picture7.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/2553.Picture8.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/2553.Picture8.png new file mode 100644 index 00000000..cec97fe8 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/2553.Picture8.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/5153.Picture1.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/5153.Picture1.png new file mode 100644 index 00000000..aa9b7263 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/5153.Picture1.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/5545.Picture9.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/5545.Picture9.png new file mode 100644 index 00000000..b9f5764f Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/5545.Picture9.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/5582.Picture10.png b/content/docs/NAVPatterns/patterns/creating-custom-charts/5582.Picture10.png new file mode 100644 index 00000000..353d5fdf Binary files /dev/null and b/content/docs/NAVPatterns/patterns/creating-custom-charts/5582.Picture10.png differ diff --git a/content/docs/NAVPatterns/patterns/creating-custom-charts/index.md b/content/docs/NAVPatterns/patterns/creating-custom-charts/index.md new file mode 100644 index 00000000..64d58fd4 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/creating-custom-charts/index.md @@ -0,0 +1,211 @@ ++++ +title = "Creating Custom Charts" +weight = 420 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Nikola Kukrika at Microsoft Development Center Copenhagen_ + +## Abstract + +The goal of this solution is to enable you to: + +1. Use charts in the web client. +2. Create charts with custom functionality. + +## Description + +This pattern enables you to implement a business chart (Specific Chart type) in a way that is maintainable and reusable on other pages. This also enables you to provide specific functionality that is not possible with the Generic Chart type and it enables you to show charts in the web client. + +The Business Chart add-in is a special because it is a combination of .NET and Javascript add-ins depending on the display target. In the web client, it renders a JavaScript control, while in the win client, it renders a .Net control. Because of this behavior, you can expect minor differences in how the chart is presented in the win client versus in the web client. Note that this implementation is specific to NAV platform code, because it is not possible to create add-ins that combines .NET and JavaScript by using a framework API. + +[![ ][image0]][anchor0] + +Example of the same chart in the win client: + +[![ ][image1]][anchor1] + +The most obvious differences in chart rendering in the two clients are: Slightly different line heights, slightly different chart height, legends in web-client charts can be used as toggle filters to show/hide groups (this is not possible in the win client). + +### Implementation Overview + +[![ ][image2]][anchor2] + +### Add-in Buffer Table + +This table is used to encapsulate the logic of the Business Chart Add-in. The table handles the following logic: + +* Storing chart values and conversion from .NET to C/AL and vice versa +* Handling of captions: We must use C/AL to provide multilanguage text in add-ins. In addition, the multilanguage text must be encapsulated in a single place, because we pass/read the same dataset from/to the add-in. +* DrillDown logic +* Other helper data related functions, for displaying date, periods, etc.[Bogdana1] [NK2] [NK3] + +**Note:** It is recommended that you reuse the **Business Chart Buffer table** (485) as a buffer table or extend. It is a generic table which should cover most of the use cases. Implement a new buffer table only if this table does not meet your needs. + +### CardPart page + +The CardPart page hosts the Business Chart add-in and must use the add-in buffer table as a source table. + +On the page, you must implement the following triggers: + +* **AddInReady** -- Executed when the page is done rendering. Used to initialize the add-in. +* **DataPointClicked** -- Single-click on an element on the chart. +* **DataPointDoubleClicked** -- Double-click on an element on the chart + +The CardPart usually contains a StatusText variable to provide more information about the chart or dataset and a set of actions to control the chart. + +The most commonly used actions are: + +* **Select Chart**, **Previous Chart**, **Next Chart** +* **Set Period**, **Work Date** +* Actions to filter the data set +* **Refresh** +* **Chart Information** -- a tooltip with a description of the chart and how data is calculated. + +### Optional: Preserving User Personalization + +One of the most common functionalities is personalization. If the chart can be customized by the user, you should store the settings that the user has entered and apply them the next time the chart is loaded. + +To do this, you need the following: + +* A setup record to store the data. You can use the **Business Chart User Setup** table (487) or create a new setup table if you need to store more information. +* A management codeunit to write/apply the settings to the chart and to encapsulate other logic. Since we should not write code on pages, the code for the actions and other logic that does not apply to the setup record should go in this codeunit. +* Setup pages where users can customize how the chart is shown and set different settings. + +The relation between the components is visualized in the following diagram: + +[![ ][image3]][anchor3] + +### Optional: Show Multiple Charts within a Single CardPart + +This option is useful on a Role Center where you want to show multiple charts using different datasets within a single part. In that case, you need a record to store the last chart selection, the setup records, and code units for separate charts. + +See, for example, the implementation of the **Mini Generic Chart** page (1390), which uses **MiniChartManagment** CodeUnit to manage separate management codeunits for charts and their setup records. The last selected chart is stored in a separate table, **Mini Chart Definition** (1310). + +[![ ][image4]][anchor4] + +## Usage + +To implement the pattern, create a new ChartPart and set the source table to **Business Chart Buffer**. + +Add a field named **BusinessChart** and set the ControlAddIn property to Microsoft.Dynamics.Nav.Client.BusinessChart. + +Then implement the AddInReady event. This event is executed when the page is done rendering. Code within this method must call the Update method from the **Business Chart Buffer** table, Update(CurrPage.BusinessChart) to initialize the chart and assign initial values. + +If you need a setup record and codeunit, then it is a good idea to encapsulate this logic within a method. + +### NAV Specific Example 1 + +Implementation of the **Finance Performance Chart** page (762) + +```al +BusinessChart::AddInReady() + +UpdateChart(Period::" "); + +LOCAL UpdateChart(Period : ',Next,Previous') + +MoveAndUpdateChart(Period,0); + +LOCAL MoveAndUpdateChart(Period : ',Next,Previous';Move : Integer) + +AccSchedChartManagement.GetSetupRecordset(AccountSchedulesChartSetup,AccountSchedulesChartSetup.Name,Move); + +AccSchedChartManagement.UpdateData(Rec,Period,AccountSchedulesChartSetup); + +Update(CurrPage.BusinessChart); + +StatusText := GetCurrentSelectionText("Period Filter Start Date","Period Filter End Date"); +``` + +In the MoveAndUpdateChart method, the AccSchedChartManagement codeunit gets a setup record and updates it if necessary. Then, it initializes the chart with setup data and sets the StatusText to show the period for which data is displayed. The same method is used by the actions to move and update the chart so that there is no code duplication. + +The following code is used to implement **DataPointClicked** + +```al +BusinessChart::DataPointClicked(point : DotNet "Microsoft.Dynamics.Nav.Client.BusinessChart.BusinessChartDataPoint") + +SetDrillDownIndexes(point); + +AccSchedChartManagement.DrillDown(Rec,AccountSchedulesChartSetup); +``` + +SetDrillDownindexes is a method from the **Business Chart Buffer** table that maps the DotNet point variable to C/AL data, so it must be used. The next method that you must implement is the action to be performed on Drilldown. + +The **DataPointDoubleClicked** trigger has the same implementation logic as the DataPointClicked trigger. + +### NAV Specific Example 2 + +Implementation of chart part 1390 on the **Small Business Role Center** page (9022) + +[![ ][image5]][anchor5] + +This chart part contains data from multiple charts within a single part. The **Status Text** field shows the name of the chart and the current period. Users can browse through the charts with **Next Chart** and **Previous Chart** or use **Select Chart** to choose from a list of available charts. + +[![ ][image6]][anchor6] + +On this dialog, users can choose if a chart should be enabled or disabled. If the chart is not enabled, it will be skipped on the **Previous Chart** and **Next Chart** actions. Charts used by this part use different codeunits and setup records. If the user changes the selected chart, this option will be saved and applied next time role center is opened. + +Users can also change the period length. + +[![ ][image7]][anchor7] + +Choosing the **Chart Information** button opens a short description of the chart. + +[![ ][image8]][anchor8] + +## NAV Usages + +Implementation of multiple charts within a single part: + +* Page 1390, **Mini Generic Chart** + +Charts that use a setup record and select the chart with **Customize Chart Setup** pages: + +* Page 772, **Inventory Performance** +* Page 771, **Purchase Performance** +* Page 770, **Sales Performance** +* Page 762, **Finance Performance** + +Chart that uses the **Business Chart User Setup** table: + +* Page 768, **Aged Acc. Receivable Chart** + +Other implementations: + +* Page 972, **Time Sheet Chart** +* Page 869, **Cash Flow Chart** +* Page 760, **Trailing Sales Orders Chart** + +## Ideas for improvement + +We should consider making a generic table for the last chart that the user has used. + +We should investigate if we could make generic code for selecting periods and other common functionality by using RecordRefs. + +Add-In improvements -- Different ways to visualize the data and to pick colors for categories. + +As a nice-to-have feature, we could implement functionality to cycle through the charts with a timer. + + + +[anchor0]: 5153.Picture1.png +[anchor1]: 1411.Picture2.png +[anchor2]: 1781.Picture5.png +[anchor3]: 0246.Picture6.png +[anchor4]: 1803.Picture7.png +[anchor5]: 1541.Picture7.png +[anchor6]: 2553.Picture8.png +[anchor7]: 5545.Picture9.png +[anchor8]: 5582.Picture10.png + + +[image0]: 5153.Picture1.png +[image1]: 1411.Picture2.png +[image2]: 1781.Picture5.png +[image3]: 0246.Picture6.png +[image4]: 1803.Picture7.png +[image5]: 1541.Picture7.png +[image6]: 2553.Picture8.png +[image7]: 5545.Picture9.png +[image8]: 5582.Picture10.png diff --git a/content/docs/NAVPatterns/patterns/cross-session-events/PubSub.png b/content/docs/NAVPatterns/patterns/cross-session-events/PubSub.png new file mode 100644 index 00000000..ff915cd2 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/cross-session-events/PubSub.png differ diff --git a/content/docs/NAVPatterns/patterns/cross-session-events/index.md b/content/docs/NAVPatterns/patterns/cross-session-events/index.md new file mode 100644 index 00000000..b9aecd0b --- /dev/null +++ b/content/docs/NAVPatterns/patterns/cross-session-events/index.md @@ -0,0 +1,170 @@ ++++ +title = "Cross Session Events" +weight = 430 +tags = ["C/AL"] +categories = ["Pattern"] ++++ + +_By Nikolai L'Estrange, from TVision Technology Ltd. in the UK_ + +## Abstract + +Track things that happen in other NAV Sessions. + +[![ ][image0]][anchor0] + +## Problem + +In Microsoft Dynamics NAV you can fire a function whenever something changes within your session (and from NAV 2016 this is even easier with the new Event model), however there is not an easy way to know what is happening in other sessions. Sometimes you would like to know what has happened since your last read, without reading everything again, e.g. when you need to pass a large dataset to a Control Add-in. + +A common way of handling this with Ledger Tables is to make note of the last record you read, and continuously poll to see if there are any new records. However this is restricted to strictly sequentially entered tables. + +## Solution + +There is a common pattern in many other languages called [Publish-Subscribe][anchor1] (or PubSub) that solves the same issue. We can implement the same pattern in NAV using a Table as a message queue platform and polling this table. We have named this pattern "Cross Session Events" in order to avoid confusion with the standard NAV Events which use the terms Publisher and Subscriber, and to try and describe more accurately when you would need this pattern. + +The pattern has four components: + +* **Publisher(s)**: These push messages to the Message Broker. +* **Subscriber Records**: Identifies the Subscriber and store filters to say what messages the Subscriber and interested in receiving. +* **Message Broker**: This distributes all messages sent in to all Subscribers that have expressed an interest (i.e. the message is within their filters). +* **Message Queue**: To hold the messages for each Subscriber. Generally once these messages are read, they are deleted. + +## Example + +An example of this would be when we have multiple users looking at the same set of data and we want their screens to update in "real time" whenever one of them makes a change, without doing a full refresh. We will use the [Observer pattern][anchor2] to capture the change (act as the Publisher) and then create a Table to hold Subscribers and Filters (Change Observer), a Table to be the Message Queue (Change Notification), and a Codeunit to be the Message Broker and help with the polling (ObserverMgt). + +Below are the table definitions: + +**Change Observer:** | | +-----|------|----- +"Table ID" | Integer | "Observable Table" +"Server ID" | Integer +"Session ID" | Integer + +**Change Notification:** | | +-----|------|----- +"Table ID" | Integer | "Observable Table" +"Server ID" | Integer +"Session ID" | Integer +"Entry No." | Integer | AutoIncrement +"Type of Change" | Option | Insert,Modify,Delete,Rename +"Record ID" | RecordID +... (other fields to indicate what has changed) + +The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key. + +The Change Notification table then has the same three fields plus an Entry No. as its Primary Key, and in this example borrows heavily from the Change Log code to fill in the rest of the message. + +_**Note:**_ Other examples of the pattern could have very different fields to identify the Subscriber, Filters and then whatever fields needed for content of the Message. + +Our Message Broker Codeunit will also serve as a central place to create Subscribers (Listen and StopListening functions) and a place to Poll for Messages. Note that the Poll function deletes the Messages as it reads them. +```al +Listen(TableID : Integer) +WITH Observer DO BEGIN + "Table ID" := TableID; + "Server ID" := SERVICEINSTANCEID; + "Session ID" := SESSIONID; + INSERT(TRUE); + COMMIT; +END; + +StopListening(TableID : Integer) +WITH Observer DO BEGIN + RESET; + SETRANGE("Server ID",SERVICEINSTANCEID); + SETRANGE("Session ID",SESSIONID); + SETRANGE("Table ID",TableID); + DELETEALL(TRUE); + COMMIT; +END; + +NotifyAll(ChangeNotification : Record "Change Notification") +WITH Observer DO BEGIN + RESET; + SETRANGE("Table ID",ChangeNotification."Table ID"); + IF FINDSET THEN REPEAT + Notify(Observer,ChangeNotification); + UNTIL NEXT = 0; +END; + +Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification") +WITH ChangeNotification DO BEGIN + "Server ID" := Observer."Server ID"; + "Session ID" := Observer."Session ID"; + "Entry No." := 0; + INSERT; +END; + +Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification") +WITH ChangeNotification DO BEGIN + TempChangeNotification.RESET; + TempChangeNotification.DELETEALL; + + RESET; + SETRANGE("Table ID",TableID); + SETRANGE("Server ID",SERVICEINSTANCEID); + SETRANGE("Session ID",SESSIONID); + + IF FINDSET THEN REPEAT + TempChangeNotification := ChangeNotification; + TempChangeNotification.INSERT; + MARK(TRUE); + UNTIL NEXT = 0; + + MARKEDONLY(TRUE); + DELETEALL; +END; +``` + +The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page: + +```al +OnQueryClosePage(CloseAction : Action None) : Boolean +ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking"); + +Timer::AddInReady() +IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN +CurrPage.Timer.Ping(1000); + +Timer::Pong() +CallUpdate; +CurrPage.Timer.Ping(1000); + +LOCAL CallUpdate() +ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification); + +WITH TempChangeNotification DO BEGIN + IF FINDSET THEN REPEAT + IF "Type of Change" = "Type of Change"::Delete THEN BEGIN + ... + END ELSE IF RecRef.GET("Record ID") THEN BEGIN + ... + END; + UNTIL NEXT = 0; +END; +``` + +## Consequences + +The PingPong control is only available on the Windows Client, so if you want to use another client you will need to use another solution to Poll for Messages. Therefore this pattern is not always going to be "real time". + +## Related Topics + +This pattern was originally described in the following blog: + +[https://geeknikolai.wordpress.com/2015/10/30/pubsub-pattern-in-dynamics-nav-2016/][anchor3] + +Below is the Wikipedia link to the PubSub pattern + +[https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern][anchor1] + + + +[anchor0]: PubSub.png +[anchor1]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern +[anchor2]: /navpatterns/1-patterns/observer/ +[anchor3]: https://geeknikolai.wordpress.com/2015/10/30/pubsub-pattern-in-dynamics-nav-2016/ + + +[image0]: PubSub.png diff --git a/content/docs/NAVPatterns/patterns/currently-active-record/6545.Table.png b/content/docs/NAVPatterns/patterns/currently-active-record/6545.Table.png new file mode 100644 index 00000000..84f5db39 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/currently-active-record/6545.Table.png differ diff --git a/content/docs/NAVPatterns/patterns/currently-active-record/index.md b/content/docs/NAVPatterns/patterns/currently-active-record/index.md new file mode 100644 index 00000000..ad7e9e2e --- /dev/null +++ b/content/docs/NAVPatterns/patterns/currently-active-record/index.md @@ -0,0 +1,155 @@ ++++ +title = "Currently Active Record" +weight = 450 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Authors: Henrik Langbak and Kim Ginnerup, Bording Data_ + +## Abstract + +Date controlled data is expensive to find in the database. This pattern describes how using a view with a sub-select and a linked table object will minimize the returned dataset. +A side effect is reduced and simplified code, increased performance and a more scalable solution that is almost independent of the amount of records in the table. + +## Description + +There is no way in NAV to get a set of records from the database, which all have the newest starting date, that is less than or equal to today's date. Having an ending date on the record will help, but it introduces some other problems. In Dynamics NAV this is normally done by reading too many records, either at the SQL Server level or in the middle tier and throw away the ones you do not need. That is a waste of resources: + +* The SQL Server is reading too many records + +* There would be too much data sent over the network. +(If the SQL Server and the NAV Service tier are on different machines.) + +* The NAV Service Tier receives and throws away data. + +## Ending Date Problem + +Ending Date may introduce some problems of its own. + +If your design requires to have one and only one active record per key in a dataset, then Ending Date introduces the possibility for overlapping or holes in the timeline. +Ending Date creates a dependency between two records. Changing a Starting Date, requires you to update the previous record. Changing the Ending Date requires you to update the next record. +If you add a record in between you will have to update both the before and the after record. + +The pattern we describe here will work whether there is an Ending Date or Not. + +The pattern is also relevant for other types than date. The pattern is usable whenever you have dependencies between rows in a table. + +Use the pattern whenever you read a set of data containing a Starting Date and you need to implement a loop to throw away unwanted records. An example could be Codeunit 7000 "Sales Price Calc. Mgt.". In this codeunit there are many loop constructs to find prices and discounts. + +## Usage + +In the following example, we have a fictive table containing: Code, Starting Date and Price. The Primary Key consist of Code, Starting Date. The Database is the Demo Database, and the Company is Cronus. + +[![ ][image0]][anchor0] + + +### 1. Create the view + +You will need to create the view before you define the Table Object. +You will need to create a view for every company in the database. +```sql +CREATE VIEW [dbo].[CRONUS$PriceView] +AS +SELECT [Code], [Starting Date], [Price] +FROM dbo.[CRONUS$Price] AS A +WHERE [Starting Date] = +(SELECT MAX([Starting Date]) +FROM dbo.[CRONUS$Price] AS B +WHERE B.[Code] = A.[Code] AND +B.[Starting Date] <= GETDATE()) +``` + +Test the view to ensure that you get the correct result. It is much easier to test now than later. + +### 2. Create the Table object + +Remember to set the link table property before you save it. + +### 3. Implement the code + +```al +IF PriceView.FINDSET THEN // You have them +``` + +### 4. Create a deployment codeunit + +Create a SQL Deployment codeunit to manage your views. +The codeunit needs to Create or Alter the views for all companies. +To see an example of how to talk to SQL Server using .NET see waldo's blog here: +[http://dynamicsuser.net/blogs/waldo/archive/2011/07/19/net-interop-calling-stored-procedures-on-sql-server-example-1.aspx][anchor1] + +### 5. Deployment + +You need to deploy in three steps: + +1. Delete the table objects referencing the views +2. Deploy and run the deployment codeunit +3. Deploy the new table objects that reference the views + +### General precaution + +If you later want to change the view, you need to follow these rules: + +* If you add columns, you need to add them to the view first and then add them to the Table Object. +* If you want to remove columns from the view, you need to delete the Table Object, then change the view and last recreate the Table Object without the new columns. + +### Code example that accomplish the same but without using the pattern + +This following example will give you the same result but the performance will deteriorate as time goes by and you get more and more old data. +```al +Price.SETCURRENTKEY(Code,"Starting Date"); +Price.SETFILTER("Starting Date",'..%1', TODAY +IF Price.FINDSET THEN BEGIN + REPEAT + Price.SETRANGE(Code, Price.Code); + Price.FINDLAST; + Price.SETRANGE(Code); + PriceTemp := Price; + PriceTemp.INSERT; + UNTIL Price.NEXT = 0; +END; +// PriceTemp will contain the Prices +``` + +### Comparison + +The above NAV example is for a very simple date controlled solution and is provided to give an idea of what the pattern changes seen from a NAV development point of view. But consider the following: + +* The table has a more complex key. +This will require setting and clearing more filters +* You need to read from more than one table. +Say you need to apply discount from a separate table. +This may give several lines in PriceTemp. +* If the Code field is controlled by a Type field. +The Code field reference keys in different tables + +All three examples above can be implemented directly in the view. By using the pattern, it will still only require a single line of NAV code. + +Using the pattern will only issue one SQL call and thereby one trip to the server. +The NAV Example will require an unknown number of SQL calls and thereby an unknown number of trips to the server. The number of SQL calls is dependent on the number of distinct Code values. +The NAV example will require SQL Server to read all data older than or equal to TODAY, but only return one row per Code. Over time, as old data piles up in the system, the NAV code will perform slower because the SQL statements will be slower. +The Pattern makes a scalable solution with a predictable performance. The performance will not deteriorate at the same rate as the NAV code example. + +## NAV Usages + +The pattern does not exist in NAV (yet J). We have used it several times in our code for an Add-On. + +## Ideas for improvement + +Query Object should be able to handle sub-selects and Unions. A simple solution could be to allow the NAV developer to specify the actual Select statement inside the query Object in clear text. Opening up for writing your own queries and map the projection to the Query-defined fields will make the query Object very versatile and remove the pressure from Microsoft trying to create all the different permutations that a select statement can have. Microsoft and others have all tried to create wizards that can create SQL select statement. They all end up having a clear text option. + +An alternative would be better support for linked table objects, specifically views. The current implementation is very fragile. + +The pattern only supports fetching data for a given date (normally today). This is because you cannot control the where-clause of the sub-select. + +## Related Topics + +The idea of having a linked table object pointing to a view could be a pattern of its own. + + + +[anchor0]: 6545.Table.png +[anchor1]: http://dynamicsuser.net/blogs/waldo/archive/2011/07/19/net-interop-calling-stored-procedures-on-sql-server-example-1.aspx + + +[image0]: 6545.Table.png diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/8308.logo.png b/content/docs/NAVPatterns/patterns/data-migration-facade/8308.logo.png new file mode 100644 index 00000000..443a891f Binary files /dev/null and b/content/docs/NAVPatterns/patterns/data-migration-facade/8308.logo.png differ diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/NoStagingTableNew2.png b/content/docs/NAVPatterns/patterns/data-migration-facade/NoStagingTableNew2.png new file mode 100644 index 00000000..67784877 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/data-migration-facade/NoStagingTableNew2.png differ diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/StagingTableNew2.png b/content/docs/NAVPatterns/patterns/data-migration-facade/StagingTableNew2.png new file mode 100644 index 00000000..78061079 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/data-migration-facade/StagingTableNew2.png differ diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling1.png b/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling1.png new file mode 100644 index 00000000..3f649988 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling1.png differ diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling2.png b/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling2.png new file mode 100644 index 00000000..c6922a1a Binary files /dev/null and b/content/docs/NAVPatterns/patterns/data-migration-facade/errorhandling2.png differ diff --git a/content/docs/NAVPatterns/patterns/data-migration-facade/index.md b/content/docs/NAVPatterns/patterns/data-migration-facade/index.md new file mode 100644 index 00000000..e4e5aa4e --- /dev/null +++ b/content/docs/NAVPatterns/patterns/data-migration-facade/index.md @@ -0,0 +1,290 @@ ++++ +title = "Data Migration Façade" +weight = 480 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By David Bastide and Soumya Dutta at Microsoft Development Center Copenhagen_ + +[![ ][image0]][anchor0] + +## Context: + +This pattern is describing how you can migrate data using the Data Migration Façade. + +## Problem: + +Writing migration code from an external source, such as a product from a competitor, can be time consuming, as you need to tackle the problems of what to migrate, in which order, exception handling... and can result in code that is fragile due to deep dependencies on the NAV data model (high coupling). Any change to the NAV data model can easily break this code. + +## Solution: + +The idea of the Data Migration Façade is to provide an API that creates and updates master data and related entities (including transactions) without referencing NAV tables. + +Additionally, the framework around the Data Migration Façade provides tools for error handling, and a way to integrate the migration in the Data Migration Overview page (page 1799). + +The façade framework has the following components: + +* Two management codeunits: + * **Data Migration Façade** (codeunit 6100): Integrates the extension to the Data Migration Wizard. Starts a migration, or restarts a migration that failed for some records. + * **Data Migration Status Facade** (codeunit 6101): Initializes and updates the status of the migration. The status displays in the **Data Migration Overview** page (page 1799). + +* Several master data migration façade codeunits that create and update entities. Each codeunit also contains events that help ensure that data is created in the correct order: + * **GL Acc. Data Migration Façade** (codeunit 6110) + * **Vendor Data Migration Façade** (codeunit 6111) + * **Customer Data Migration Façade** (codeunit 6112) + * **Item Data Migration Façade** (codeunit 6113) + * If you want to migrate other entities, it is possible to define your own codeunit that will contain your custom code (see 'Usage' below). + +* A page where you can view the status and progress of the migration. It shows one line for each master data entity (item, customer, vendor, general ledger account) that was chosen for migration. You can also stop a migration by choosing the **Stop Migration** action. + + * **Data Migration Overview** (page 1799) + +## Usage: + +There are the following use cases: + +* Migration with staging tables, where data from another product is exported to a file or set of files, and the exported data is then imported to buffer tables, before running the migration logic. In this case, the migration is implemented in your extension codeunits, and it is called through events, one record at a time. + +* Migration without staging tables, for example, when you migrate data by importing it from an external tool such as external APIs and webservices. In this case, the migration is also implemented in your extension codeunits but it will be called through the OnRun procedure. You will be responsible for looping on the records to migrate, and you must migrate all records in this unique OnRun call for a given entity. + +To initialize and start the data migration, you must call the following procedures: + +* **"Data Migration Status Façade".InitStatusLine('My Migration Type',Database::Item,42000,0,Codeunit::"My extension Item migration codeunit")** +This deletes existing status lines for migrating Items for 'My Migration Type', and initializes a new status line with 0 records migrated out of 42000\. + +**"Data Migration Façade".StartMigration('My Migration Type',false)** +This starts the migration. False means this is not a retry. A re-try is when you migrate one or more records from the **Show Errors** page, which is described later in this document. Retry = true is only used by the **Show Errors** page and should not be used from extensions. + +### Usage without staging tables: + +The overall workflow is: + +1. Integrate your extension in the **Data Migration Wizard** by subscribing to the events exposed by the **Data Migration Façade**. +2. From there, initialize the status of the migration so it can be displayed in the Data Migration Overview. When initializing the status lines, you provide the codeunit ID that will be called for each entity: **"Data Migration Status Façade".InitStatusLine('My Migration Type',Database::Item,42000,0,Codeunit::"My extension Item migration codeunit")**. +3. Launch the migration: **"Data Migration Façade".StartMigration('My Migration Type',false).** +4. Your migration codeunits are called one at a time (**OnRun**) in the following order: + 1. G/L accounts (first, because customer/vendor posting groups refer to G/L accounts) + 2. Customers + 3. Vendors + 4. Items (because discounts may refer to customer groups) + 5. Others +5. Loop on all of the records to migrate. You can update the increment of the amount of records migrated: **"Data Migration Status Facade".IncrementStatusLine('My Migration Type',Database::Item,42).** + +[![ ][image1]][anchor1] + +_Figure 1: sequence diagram of the data migration without staging tables_ + +The following example shows how to migrate items without staging tables: +```al +trigger OnRun(); +var + ItemDataMigrationFacade: Codeunit "Item Data Migration Facade"; + ItemNumber: Integer; + ItemJson: Text; +begin + // loop on items retrieved through a web service for example + for ItemNumber := 1 to ExternalWebService.GetItemCount do begin + ExternalWebService.GetItem(ItemNumber,ItemJson); + + // create item using the facade + if not ItemDataMigrationFacade.CreateItemIfNeeded(ItemJson.ItemNumber,ItemJson.ItemName1, + ItemJson.ItemName2,ConvertItemType(ItemJson.ItemType)) then + exit; // item already exists + + // set some fields using the facade + ItemDataMigrationFacade.SetVendorItemNo(ItemJson.VendItemNumber); + ItemDataMigrationFacade.SetUnitVolume(ItemJson.Volume); + ItemDataMigrationFacade.SetAlternativeItemNo(ItemJson.AltItemNumber); + if ItemJson.PrimaryVendor <> '' then + ItemDataMigrationFacade.SetVendorNo(ItemJson.PrimaryVendor); + + // migrate dependencies + MigrateItemUnitOfMeasure(ItemDataMigrationFacade,ItemJson); + + // modify the item (+run trigger) to save the changes made by setters + ItemDataMigrationFacade.ModifyItem(true); + // update the status in the migration dashboard + DataMigrationStatusFacade.IncrementMigratedRecordCount('My Migration Type',Database::Item,1); + end; +end; + +procedure MigrateItemUnitOfMeasure(ItemDataMigrationFacade : Codeunit "Item Data Migration Facade"; ItemJson : Text) +var + MyUnitCodeStagingTable: Record "My Unit Code Staging Table"; + DataMigrationStatusFacade: Codeunit "Data Migration Status Facade"; + DescriptionToSet: Text[10]; + UnitCodeJson: Text; +begin + if ItemJson.UnitCode = '' then + // log an error using the Data migration façade + DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase( + 'My Migration Type',Database::Item,'Unit of measure is empty.'); + + if ExternalWebService.GetUnitCode(ItemJson.UnitCode,UnitCodeJson) then + DescriptionToSet := UnitCodeJson.Description; + ItemDataMigrationFacade.CreateUnitOfMeasureIfNeeded(ItemJson.UnitCode, DescriptionToSet); + + // set the unit of measure on the item + ItemDataMigrationFacade.SetBaseUnitOfMeasure(ItemJson.UnitCode); +end; +``` + +_Figure 2: Example of Item and Item Unit of Measure migration without staging tables_ + +### Usage with staging tables: + +The overall workflow is: + +* Integrate your extension in the Data Migration Wizard by subscribing to the events exposed by the **Data Migration Façade**. + +* From there, initialize the status of the migration so it can be displayed in the **Data Migration Overview: "Data Migration Status Façade".InitStatusLine('My Migration Type',Database::Item,42000,Database::"My extension Staging table for items",0)**. +* You can either fill the staging tables from the wizard events, or in a subscriber to the event dedicated to filling staging tables (**'OnFillStagingTables'** in codeunit 6100): in this case, the import of data from the files to the staging tables will be done in the background. +* Launch the migration: **"Data Migration Façade".StartMigration('My Migration Type',false)**. +* Subscribe to the events to migrate entities and their dependencies. +* From the event subscribers, call the façade procedures to create entities and set their field values: + + * **"Item Data Migration Façade".CreateItemIfNeeded('ITEM042','My Item Description';'My Item Description 2';ItemTypeToSet::Inventory)** + * **"Item Data Migration Façade".SetBaseUnitOfMeasure('BOX')** + * **"Item Data Migration Façade".ModifyItem(true)** + +[![ ][image2]][anchor2] + +_Figure 3: Simplified sequence diagram of the data migration with staging tables_ + +Below is a simplified example showing how to create an item: +```al +[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItem', '', true, true)] +procedure OnMigrateItem(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId) +var + MyItemStagingTable : Record "My Item Staging Table"; +begin + // handle the event if it targets this extension's staging table + if RecordIdToMigrate.TableNo <> Database::"My Item Staging Table" then + exit; + MyItemStagingTable.Get(RecordIdToMigrate); + + // create item using the facade + if not Sender.CreateItemIfNeeded(MyItemStagingTable.ItemNumber,MyItemStagingTable.ItemName1, + MyItemStagingTable.ItemName2,ConvertItemType(MyItemStagingTable.ItemType)) then + exit; // item already exists + + // set some fields using the facade + Sender.SetVendorItemNo(MyItemStagingTable.VendItemNumber); + Sender.SetUnitVolume(MyItemStagingTable.Volume); + Sender.SetAlternativeItemNo(MyItemStagingTable.AltItemNumber); + if MyItemStagingTable.PrimaryVendor <> '' then + Sender.SetVendorNo(MyItemStagingTable.PrimaryVendor); + + // modify the item (+run trigger) to save the changes made by setters + Sender.ModifyItem(true); +end; +``` + +_Figure 4: Example of event subscriber for Item migration_ + +Below is another example showing how to use additional events to set fields that reference other tables, here the unit of measure: + +```al +[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItemUnitOfMeasure', '', true, true)] +procedure OnMigrateItemUnitOfMeasure(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId) +var + MyItemStagingTable : Record "My Item Staging Table"; + MyUnitCodeStagingTable : Record "My Unit Code Staging Table"; + DescriptionToSet: Text[10]; +begin + // handle the event if it targets this extension's staging table + if RecordIdToMigrate.TableNo <> Database::"My Item Staging Table" then + exit; + + MyItemStagingTable.Get(RecordIdToMigrate); + if MyItemStagingTable.UnitCode = '' then + error('Unit of measure is empty.'); + MyUnitCodeStagingTable.SetRange(UnitCode,MyItemStagingTable.UnitCode); + if MyUnitCodeStagingTable.FindFirst then + DescriptionToSet := MyUnitCodeStagingTable.Description; + + // create the unit of measure through the facade + Sender.CreateUnitOfMeasureIfNeeded(MyItemStagingTable.UnitCode, DescriptionToSet); + + // set the unit of measure on the item + Sender.SetBaseUnitOfMeasure(MyItemStagingTable.UnitCode); + + // modify the item to save the changes made by setter + Sender.ModifyItem(false); +end; +``` + +_Figure 5: Example of event subscriber for Item Unit of Measure migration_ + +### Combining both approaches: + +If you want to migrate additional entities, the **Data Migration** framework lets you initialize the migration with entities other than master data. In this case, the **Data Migration Overview** page will show additional lines. Item, vendor, customer, an general ledger accounts are migrated with an event driven approach, and the additional entities are migrated by calling an extension codeunit **OnRun** method. + +## Error handling with staging tables: + +The migration starts by calling **RUN** on the façade codeunit. Errors thrown during the call are captured by **GETLASTERRORTEXT** and displayed when you choose the **Show Errors** action on the **Data Migration Overview** page. + +[![ ][image3]][anchor3] + +_Figure 6: List of errors shown when clicking **Show Errors** on the **Data Migration Overview** page_ + +The **Edit Record** action opens a view of the staging table, where you can edit fields to fix errors. Figure 4 shows an example of a page for the vendor staging table in a migration from C5\. + +[![ ][image4]][anchor4] + +_Figure 7: Edit a staging table record_ + +The **Staging Table ID** determines the page to open, so it is important that the page ID is equal to the **Staging Table ID**, at least for the master data staging tables, for example, for **G/L Accounts, Items, Customers** and **Vendor**. You should ensure that pages to edit related entities are linked on this page by means of new actions. For example, Figure 4 uses the **C5 Purchaser** action. + +After you fix the staging table record, you can choose the **Migrate** action to mark the selected records as records to retry, and then run **StartMigration** with the **Retry** flag set to true. This is the only place where the retry flag should be set to true in the **StartMigration** procedure. + +Error handling without staging tables + +When migrating data without staging tables, errors can be registered manually by the extension using **DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase**. Otherwise they can be registered automatically if the codeunits fail when called on their **OnRun** procedure. + +Errors will be displayed in the error list, but you cannot open and edit records because there is no staging table. The Edit action will not be available. + +## Limitations: + +* Data migration will fail if there are customers, vendors, items in the database and if these entities are selected for migration. For example, if you choose to migrate items and your company already contains items, you will get an error. This should not be an issue if you migrate your data from another tool to NAV, in which case you will most likely start on a fresh empty company. However, if you just want to import additional items to a company with existing items, then it is not supported by the framework. however, you can still use the different functions provided by the different codeunits (such as **Item Data Migration Facade**) to create the entities without strong coupling on the NAV data model. +* **G/L entries** are deleted automatically. + +* There is no automated rollback in case of failure: data that is successfully migrated will be commited, and data that is not successfully migrated with be shown in the errors list. The retry feature (in case of staging tables) then makes it possible for you to retry individual entities or ignore them. + +## Usages in NAV: + +The Data Migration Façade is available starting from version 2018\. + +The following Façade codeunits for data migration management are available: + +* COD6100 (**Data Migration Facade**) + +* COD6101 (**Data Migration Status Facade**) + +The following Entity data migration façade codeunits are available: + +* COD6110 (**GL Acc. Data Migration Facade**) +* COD6111 (**Vendor Data Migration Facade**) +* COD6112 (**Customer Data Migration Facade**) + +* COD6113 (**Item Data Migration Facade**) +* COD6114 (**Ex. Rate Data Migration Facade**) + +## References: + +Façade pattern on Wikipedia: https://en.wikipedia.org/wiki/Facade_pattern + + + +[anchor0]: 8308.logo.png +[anchor1]: NoStagingTableNew2.png +[anchor2]: StagingTableNew2.png +[anchor3]: errorhandling1.png +[anchor4]: errorhandling2.png + + +[image0]: 8308.logo.png +[image1]: NoStagingTableNew2.png +[image2]: StagingTableNew2.png +[image3]: errorhandling1.png +[image4]: errorhandling2.png diff --git a/content/docs/NAVPatterns/patterns/discovery-event/Pic2.jpg b/content/docs/NAVPatterns/patterns/discovery-event/Pic2.jpg new file mode 100644 index 00000000..31139410 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/discovery-event/Pic2.jpg differ diff --git a/content/docs/NAVPatterns/patterns/discovery-event/Pic2b.jpg b/content/docs/NAVPatterns/patterns/discovery-event/Pic2b.jpg new file mode 100644 index 00000000..00ad1fc3 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/discovery-event/Pic2b.jpg differ diff --git a/content/docs/NAVPatterns/patterns/discovery-event/Pic3.jpg b/content/docs/NAVPatterns/patterns/discovery-event/Pic3.jpg new file mode 100644 index 00000000..f5c02e12 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/discovery-event/Pic3.jpg differ diff --git a/content/docs/NAVPatterns/patterns/discovery-event/Pic4.jpg b/content/docs/NAVPatterns/patterns/discovery-event/Pic4.jpg new file mode 100644 index 00000000..0d3a1677 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/discovery-event/Pic4.jpg differ diff --git a/content/docs/NAVPatterns/patterns/discovery-event/ServiceConnections.jpg b/content/docs/NAVPatterns/patterns/discovery-event/ServiceConnections.jpg new file mode 100644 index 00000000..4819bdef Binary files /dev/null and b/content/docs/NAVPatterns/patterns/discovery-event/ServiceConnections.jpg differ diff --git a/content/docs/NAVPatterns/patterns/discovery-event/index.md b/content/docs/NAVPatterns/patterns/discovery-event/index.md new file mode 100644 index 00000000..d8cbf195 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/discovery-event/index.md @@ -0,0 +1,89 @@ ++++ +title = "Discovery Event" +weight = 500 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_by waldo_ + +# Abstract + +The "Discovery Event" pattern is a way for a generic functionality, to call out to other functionalities that want to make use of it, by raising an event, so that they have an event to subscribe to. This is usually done to set itself up within the generic app. + +# The problem + +Let's suppose you have a generic piece of functionality, that hooks into lots of places (modules) in your application. To set this up, you might have to hook into all these parts of the application. Well, this pattern turns this setup around: let all the different modules set itself up in the generic app by raising a "discovery event". + +# Usage + +The pattern is most easily described when you look at an example. This example is an actual usage of the pattern within the application, in page **Service Connections.** + +[![ ][image0]][anchor0] + +The goal of this functionality is to: + +* List all the different connections to external services, +* Have a central place to navigate to the corresponding setup of the service. + +The functionality (**Service Connections**) itself, is not aware of the state nor setup nor any context of all the different services in the list. All it does is: + +* It raises an event as an opportunity for all services within the NAV application to subscribe to, +* It has a public function **InsertServiceConnection** that the subscribers can use to register itself at the Service Connection. + +The event **OnRegisterServiceConnection** is raised when the page (1279 - **Service Connections**) is opened. + +One example of a subscription is the SMTP setup. In Codeunit 400 you'll find the subscriber function **HandleSMTPRegisterServiceConnection** which subscribes to this discovery event, and calls the **InsertServiceConnection** to register itself. + +# Description + +The main idea of this pattern is: "Discover the settings, the context, the records, ... which I need for my functionality" or "Discover the configuration for my functionality". In any case, "discover" is the main idea. It's a pattern where using both publishers and subscribers in one application makes a lot of sense. +Let's break down to the steps that are needed to implement the pattern. + +## Step 1: Publish the event + +In the below example, I create a table **Module Status** with a published event **OnDiscoverModuleStatuses**. + +[![ ][image1]][anchor1] + +You see that I also include the sender. This way, I will be able to access the methods on my table (which I use as a class). Obviously, other patterns can be applied here as well, like the Argument Table pattern. + +## Step 2: Raise the event on the right place + +When you publish an event, it should obviously be raised somewhere in the code as well. In the below example, I want to raise the event simply by a method which I want to call from a page. So I create a global function where I raise the event: + +[![ ][image2]][anchor2] + +## Step 3: Create one or more global functions, so that your subscriber can call into your functionality to configure, set up, or do whatever it needs to do to make itself discoverable + +The generic functionality that I want to call, should be part of the main class - in this case the **Module Discovery** class, or better, the table (**Module Status**). In this table, I create this global function, because I want to make it available for the subscribers: + +[![ ][image3]][anchor3] +The business logic doesn't really matter for this pattern. This is obviously dependent on the functionality where you would like to implement the pattern. + +## Step 4: subscribe from the places in the app to this event, use the global function(s) + +This could be anywhere. Any module within your vertical, of within the main application, can subscribe to the event. In the example below, I create the subscriber in Codeunit80, as I was interested in the status of the Sales-module in default NAV. +The exact place of the subscriber is up to you. The main message is that it's part of the module that wants to subscribe, and not part of the **Module Status** module in the application. +Here is the subscriber (and one small helper function): + +[![ ][image4]][anchor4] +You see I can use the "sender" as a normal Record-variable. I access the previously created global function to "register" this sales-module. + +# Microsoft Dynamics NAV Versions + +This pattern only works with Microsoft Dynamics **NAV 2016 and up.** + + + +[anchor0]: ServiceConnections.jpg +[anchor1]: Pic2.jpg +[anchor2]: Pic2b.jpg +[anchor3]: Pic3.jpg +[anchor4]: Pic4.jpg + + +[image0]: ServiceConnections.jpg +[image1]: Pic2.jpg +[image2]: Pic2b.jpg +[image3]: Pic3.jpg +[image4]: Pic4.jpg diff --git a/content/docs/NAVPatterns/patterns/document/0005.Document-Pattern-UML-Class-Diagram.jpg b/content/docs/NAVPatterns/patterns/document/0005.Document-Pattern-UML-Class-Diagram.jpg new file mode 100644 index 00000000..8a6bf1f0 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/document/0005.Document-Pattern-UML-Class-Diagram.jpg differ diff --git a/content/docs/NAVPatterns/patterns/document/2086.Design-Pattern-Document-SubPage-Properties.png b/content/docs/NAVPatterns/patterns/document/2086.Design-Pattern-Document-SubPage-Properties.png new file mode 100644 index 00000000..aaa2c9a9 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/document/2086.Design-Pattern-Document-SubPage-Properties.png differ diff --git a/content/docs/NAVPatterns/patterns/document/index.md b/content/docs/NAVPatterns/patterns/document/index.md new file mode 100644 index 00000000..66120a29 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/document/index.md @@ -0,0 +1,114 @@ ++++ +title = "Document" +weight = 510 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By Xavier Garonnat, knk Ingénierie (France), xgaronnat@knk.fr_ + +## Abstract + +A document structure contains a header and a set of lines. Each line is linked to the header and could have common data with header. + +## Description + +This pattern should be used as a basis to build any document, showing a header and multiple lines in the same page. Basically, a document is at least composed of two tables and three pages, as shown below: + +[![ ][image0]][anchor0] + +## Usage + +You should use it any time you have to capture and store a document. + +## Example + +To build this example from scratch, you will need: + +* Two tables, one for the header (called "Document Header"), and one for the document lines (called "Document Line"). Each document will be composed of "1 to N" line(s). +* Three pages, one for the header, one for the subpage (lines), and the last for the document list obviously.Table "Document Header" + +**Table "Document Header"**: Is the "header" table of your document (like Sales Header, Purchase Header, Transfer Header ...) + +* Add a field "No." (Code 20): Should be the first field and primary key of your documents, to be driven by Serial No. (See corresponding design pattern) + +For this sample, I just added a "Sell-to Customer No." to this table. Don't forget to manage deletion of lines with trigger OnDelete(). + +**Table "Document Line"**: will store the lines of the document + +* Add a field "Document No." (Code 20): Should be the first field and is related to table "Document Header": set TableRelation to your "Document Header" table +* Add a field "Line No." (Integer): this field will be populated automatically by the subpage Page (see AutoSplitKey) + +First (Primary) Key must be "Document No.,Line No.". On table properties, set PasteIsValid to No (to avoid copying/pasting lines, will be implemented by "Copy document", another pattern). + +For my sample, I just add a couple of fields: "Item No." and "Quantity" to this table (just copy/paste standard fields from "Sales Line" table and delete trigger code, this will insure that each field will be well designed) + +**Page "Document Subpage"**: will display the lines in the main form, and will be in charge of assigning line number automatically. + +Create the page for table "Document Line" with the wizard by selecting the ListPart template, add all yours fields except the primary key ("Document No." and "Line No."). + +Then edit the properties: + +* Set AutoSplitKey, DelayedInsert and MultipleNewLines to Yes: this combination will make your subpage work as required. +* AutoSplitKey is used to set NAV calculate the last field of the key ("Line No.") with proper numbers (10000, 20000...). + +Set caption to "Lines". Save your page, we will use it on the next step. + +**Page "Document"**: will display the document, and the lines with subpage. + +Create the page for "Document Header" Table with the wizard by selecting the Document template: + +* Add a General FastTab +* Add all the revelant fields for the user (or at least "No.") +* Click Finish to close the wizard + +Then simply add your subpage as new line in the designer, and adjust the property "SubFormPerLink" with "Document No.=FIELD(No.)" to link header and lines : + +[![ ][image1]][anchor1] + +**Page "Document List":** Use the Page wizard to create a List page based on the Document table and add fields, FactBox (RecordLinks, Notes...), etc. + +Once created: + +* Set Editable to No on the List +* CardPageID to Page "Document" to enable New/Edit/... Pane actions. + +Save our page and add it to the Role Page "Order Processor Role Center" for example. + +Now, observe how "Line No." is calculated on the first line, and when inserting a new line between the first and second one. + +Code sample (copy link to your browser) : https://knk1fr-my.sharepoint.com/personal/xgaronnat_knk_fr/_layouts/15/guestaccess.aspx?guestaccesstoken=hL0P%2fyQ1ZreY5KlSPc%2b8dHrO4zjUkqQbg8DnGSbgd1Y%3d&docid=02b3cb93e1ff1459380891795fb8441fc + +## NAV Usages + +So many: Sales Order, Purchase Order, Transfer Order, Assembly Order... + +For posted document, it's quite similar, but you don't have to setup subpage properties like AutoSplitKey, used for data entry purpose only (and your pages content should be mainly read-only / non editable). + +## Ideas for improvement + +* A new property like "AutoSplitStartNumber", enabled if AutoSplitKey=Yes, default value with <10000\>. Allow to change the numbers of created line. +* Be able to copy/paste header AND lines or import header and line from an Excel file. + +## When it should not be used + +This pattern is mainly used for Documents, and may not be used directly for Master data or any other table (Setup, Supplemental, etc...). + +## Related Topics + +Use Series No. Pattern for your documents, and Copy Document to implement document duplication. + +## References + +Walkthrough: Creating a Document Page : [http://msdn.microsoft.com/en-us/library/dd338599(v=nav.71).aspx][anchor2] +{{< youtube S9cRD2D4c0>}} + + + +[anchor0]: 0005.Document-Pattern-UML-Class-Diagram.jpg +[anchor1]: 2086.Design-Pattern-Document-SubPage-Properties.png +[anchor2]: http://msdn.microsoft.com/en-us/library/dd338599(v=nav.71).aspx "http://msdn.microsoft.com/en-us/library/dd338599(v=nav.71).aspx" +[anchor3]: https://www.youtube.com/watch?v=S9cRD2D4c_0&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=27 + + +[image0]: 0005.Document-Pattern-UML-Class-Diagram.jpg +[image1]: 2086.Design-Pattern-Document-SubPage-Properties.png diff --git a/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/0654.easy-update-1.png b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/0654.easy-update-1.png new file mode 100644 index 00000000..9813a3cb Binary files /dev/null and b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/0654.easy-update-1.png differ diff --git a/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/4024.easy-update-2.png b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/4024.easy-update-2.png new file mode 100644 index 00000000..02b417a5 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/4024.easy-update-2.png differ diff --git a/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/index.md b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/index.md new file mode 100644 index 00000000..f652f12f --- /dev/null +++ b/content/docs/NAVPatterns/patterns/easy-update-of-setup-or-supplementary-information/index.md @@ -0,0 +1,100 @@ ++++ +title = "Easy Update Of Setup Or Supplementary Information" +weight = 520 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Anders Larsen at Microsoft Development Center Copenhagen_ + +## Abstract + +Users or the administrator must regularly update setup or supplementary information in the day-to-day business, such as setting up a new type of customer. This setup task often arrives when their focus is on registration and execution instead of setup. + +The navigation experience around these extra steps is often quite troublesome and time-consuming. To enable users to easily perform the needed update, a guide often gives the best support. + +To guide users, we can prompt them with a dialog on which they can update the setup or supplementary information instantly and easily, so that they can proceed with the business task without being side-tracked. + +## Usage + +Define two functions in the setup or supplementary table: One for verifying if the needed information is available, and another for exposing the page that contains the fields that the user must update. + +Call the code. For example: +``` +Local IsXAvailable : Boolean +If field X <> '' then +Exit(True) +Exit(false) + +VerifyAndSetX +If IsXAvailable then +Exit; +If Confirm('Field X is missing a value. Do you want to update it now?') then +Open the card page in edit mode + +If not IsXAvailable then +Error(Field X is missing a value. Please correct it.) +``` + +The calling code + +``` +.. + +SetupTable.VerifyAndSetX + +.. +``` +## NAV Specific Example + +In the **Sales & Receivables Setup** table (311) for the DK version, the following procedures have been added: + +```al +Local Procedure IsOIOUBLPathAvailable(...) + +Procedure VerifyAndSetOIOUBLPath(...) +``` + +The code in the **Sales & Receivables Setup** table can now be called directly from the related processing codeunit, such as the **Sales-Post + Print** codeunit (82). + +Were the code is called: + +```al +IF ("EAN No." <> '') THEN + SalesSetup.VerifyAndSetOIOUBLPathSetup(SalesHeader."Document Type"); +``` + +If the setup is not updated properly, the user is prompted to update it as follows. + +[![ ][image0]][anchor0] + +Choosing **Yes** opens the related setup page. + +[![ ][image1]][anchor1] + +## NAV Usages + +* Report 206, **Sales invoice** +* Table 79, **Company Information** +* In OIOUBL fields (DK version) during posting/printing of a sales invoice. + +## Ideas for Improvement + +Make a more generic platform implementation that launches the corresponding card page for Rec on Rec.testfield with an asterisk mark for the field that needs a proper value. + +## Related Topics + +The anti-pattern is to do a testfield on a field that is not in the table that you are currently updating. + +The test field message can often be confusing because the pages are often named differently than the tables, which can lead to misunderstanding and context-switching. + +{{< youtube oeASJN-zqTo>}} + + + +[anchor0]: 0654.easy-update-1.png +[anchor1]: 4024.easy-update-2.png +[anchor2]: https://www.youtube.com/watch?v=oeASJN-zqTo&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=18 + + +[image0]: 0654.easy-update-1.png +[image1]: 4024.easy-update-2.png diff --git a/content/docs/NAVPatterns/patterns/error-message-processing/image001.png b/content/docs/NAVPatterns/patterns/error-message-processing/image001.png new file mode 100644 index 00000000..82c8cfd9 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/error-message-processing/image001.png differ diff --git a/content/docs/NAVPatterns/patterns/error-message-processing/image003.png b/content/docs/NAVPatterns/patterns/error-message-processing/image003.png new file mode 100644 index 00000000..bfebcaf8 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/error-message-processing/image003.png differ diff --git a/content/docs/NAVPatterns/patterns/error-message-processing/index.md b/content/docs/NAVPatterns/patterns/error-message-processing/index.md new file mode 100644 index 00000000..07d9c27c --- /dev/null +++ b/content/docs/NAVPatterns/patterns/error-message-processing/index.md @@ -0,0 +1,108 @@ ++++ +title = "Error Message Processing" +weight = 550 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By Jesper Schulz at Microsoft Development Center Copenhagen_ + +Note: This pattern describes new functionality which makes it possible to generalize the [Journal Error Processing][anchor0] pattern. + +## Abstract + +Missing, invalid or incomplete data is a common issue during data processing in NAV. This article describes how to collect all error messages during processing using the error message component and present them to the user in a unified way, which enables the user to correct the errors efficiently. By leveraging the integrated error message logging functions, you can log a message with a single line of code and present it to the user with another one-liner. + +## Description + +This article describes how to use the Error Message component in NAV, which in short gives you the possibility to: + +1. Link an error message to the page which enables you to resolve the problem. +2. Assemble all error messages in one central view instead of having to encounter them one by one. + +Validating data is a common task during data processing in NAV. Unfortunately, validation is often done using NAV's integrated ERROR and TESTFIELD functions, which halt execution of the process. The user will then have to locate the invalid / missing data, correct it and reinitiate the process, possibly running into the next error, making the cycle repeat itself. This can be a very tedious, time-consuming and frustrating process. The error message component aims at improving this experience by providing a lightweight framework for error message logging and this article will explain how to leverage this functionality in your code. By doing so, all error messages are gathered during (pre-)processing and are finally presented to the user. The user then has the possibility to click on the error message, which will open the record where the invalid / missing data is located, thereby enabling the user to correct all mistakes efficiently, from one central place and in one go. + +The example below comes from a Mexican localization, where the user has to export financial balances and transactions into an XML file for government audit purposes. In order to generate valid files, some mandatory data needs to be entered in the system. By leveraging the error message component, the user will be presented with the following page, if missing or invalid data was discovered: + +[![ ][image0]][anchor1] + +By clicking on the error message, the user will be presented with the entity, where the invalid / missing data should be corrected / added. This is done by applying the related pattern [Easy Update of Setup or Supplementary Information][anchor2]. + +[![ ][image1]][anchor3] + +## Usage + +In the processing function, define a temporary record of type "Error Message". Use the functions on that record to populate the record with error message, a few of them being: + +* **LogIfEmpty** +* **LogIfLengthExceeded** +* **LogIfInvalidCharacters** +* **LogIfOutsideRange** +* **LogIfGreaterThan** +* **LogIfEqualTo** +* **LogMessage** + +The following parameters must be provided to these functions: + +* **Record:** The record which you want to check +* **Field Number:** The field number of the field you want to verify the value of +* **Condition:** The condition the field must meet (e.g. length, range, valid characters) +* **Message Type:** The type of message, which can be Error, Warning or Message + +When the processing is complete, you can check if any error messages of type "Error" were logged by calling the **HasErrors** function and you can show the list or error messages by calling the **ShowErrorMessages** function. You can also integrate the error messages list as a FactBox, but that is not part of this example. + +## NAV Specific Example + +The code below is an example of how the error message component was used in one part of the before mentioned Mexican feature. This code iterates over all G/L Accounts and pipes information out into an XML file. While doing so, it is validated that all mandatory fields have values and meet certain conditions. And only if that is the case, is the XML document actually exported. Also notice, that an error message is logged, in case no G/L Accounts are found given the provided filters. That way, the user can be guided to setup the system correctly. + +```AL +PROCEDURE ExportChartOfAccounts@1(Year@1000 : Integer;Month@1001 : Integer); +VAR + TempErrorMessage@1003 : TEMPORARY Record 700; +BEGIN + TempErrorMessage.ClearLog; // only necessary if variable is global + ... + CreateXMLHeader(Document,RootNode,CatalogoNodeTxt,Namespace,Year,Month,'1.1'); + + IF GLAccount.FINDSET THEN BEGIN + REPEAT + TempErrorMessage.LogIfEmpty (GLAccount,GLAccount.FIELDNO(Name),TempErrorMessage."Message Type"::Error); + + XMLDOMManagement.AddElement(RootNode,'Ctas','',Namespace,Node); + XMLDOMManagement.AddAttribute(Node,'CodAgrup',GLAccount."SAT Account Code"); + ... + CASE GLAccount."Debit/Credit" OF + GLAccount."Debit/Credit"::Debit: + XMLDOMManagement.AddAttribute(Node,'Natur','D'); + GLAccount."Debit/Credit"::Credit: + XMLDOMManagement.AddAttribute(Node,'Natur','A'); + ELSE + TempErrorMessage.LogMessage( + GLAccount,GLAccount.FIELDNO("Debit/Credit"),TempErrorMessage."Message Type"::Error, + STRSUBSTNO(GLAccountTypeErr,GLAccount."Debit/Credit",GLAccount.RECORDID)); + END; + UNTIL GLAccount.NEXT = 0; + END ELSE + TempErrorMessage.LogSimpleMessage(TempErrorMessage."Message Type"::Error,NoSATAccountDefinedErr); + + IF NOT TempErrorMessage.HasErrors(TRUE) THEN + SaveXMLToClient(Document,Year,Month,'CT'); + TempErrorMessage.ShowErrorMessages(FALSE); +END; +``` + +One could also do pre-processing in a function of its own, and only if the pre-processing results in no error messages of type "Error" would the processing continue. + +## Ideas for improvement + +By using this easy to use component, we have the possibility to extend this functionality going forward. A nice addition to the error message component would be the possibility to log the error messages persistently in a grouped manner, thereby allowing 3rd parties to see the issues the users bump into the most, or allowing 3rd parties to get an detailed insight into what happened, thereby enabling them to provide better support. + + + +[anchor0]: /navpatterns/1-patterns/journal-error-processing/ +[anchor1]: image001.png +[anchor2]: /navpatterns/1-patterns/easy-update-of-setup-or-supplementary-information/ +[anchor3]: image003.png + + +[image0]: image001.png +[image1]: image003.png diff --git a/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/0724.Headline-sequence-diagram-v2.png b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/0724.Headline-sequence-diagram-v2.png new file mode 100644 index 00000000..d6442002 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/0724.Headline-sequence-diagram-v2.png differ diff --git a/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/3733.logo.png b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/3733.logo.png new file mode 100644 index 00000000..9ebb1d36 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/3733.logo.png differ diff --git a/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/Headline.png b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/Headline.png new file mode 100644 index 00000000..56b3cd2c Binary files /dev/null and b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/Headline.png differ diff --git a/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/index.md b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/index.md new file mode 100644 index 00000000..3f5b4615 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/index.md @@ -0,0 +1,157 @@ ++++ +title = "Extending the Role Center Headlines" +weight = 560 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By David Bastide at Microsoft Development Center Copenhagen_ + +[![ ][image0]][anchor0] + +## Context + + +Headlines are designed as a page of type HeadlinePart containing at least one text field. The part is added to the top of Role Center pages. +This document provides an elegant and extensible pattern about how to extend the Role Center headlines to add your own business headlines based on your data, and display them only if relevant. + +## Description + + +The Dynamics 365 Business Central release (April 2018) introduces a new HeadlinePart page type. This page type defines a page that rotates a display of several headlines after another, in the web client. A user can also click to switch to another headline. Headlines can also include a drilldown action that will be invoked when the user clicks the headline Text of the payload can be emphasized. +Headlines are divided in 2 parts: the qualifier, and the payload as you can see in the figure below. + +[![ ][image1]][anchor1] + +_Figure 1: Qualifier, Payload and emphasized text._ + +## Usage + +A page part has been added to each major Role Center: + +* Page 9006 Order Processor Role Center, contains Page 1441 Headline RC Order Processor +* Page 9015 Job Project Manager RC, contains Page 1443 Headline RC Project Manager +* Page 9022 Business Manager Role Center, contains Page 1440 Headline RC Business Manager +* Page 9024 Security Admin Role Center, contains Page 1445 Headline RC Administrator +* Page 9026 Sales & Relationship Mgr. RC, contains Page 1444 Headline RC Relationship Mgt. +* Page 9027 Accountant Role Center, contains Page 1442 Headline RC Accountant +* Page 9028 Team Member Role Center, contains Page 1446 Headline RC Team Member +* Page 9010 Production Planner Role Center, contains Page 1447 Headline RC Prod. Planner +* Page 9016 Service Dispatcher Role Center, contains Page 1448 Headline RC Serv. Dispatcher + +You can create extensions that extend these pages to add your own headlines. +If no headline is added on these pages, fallback headlines will be displayed. + +The process to extend the headlines of a Role Center is simple: + +1. In a V2 extension, extend the pages (PAG1440 to 1446) with one or more fields you want to add as headlines. The field and its visibility should be variables that are populated in OnAfterGetRecord. +2. Subscribe to the OnComputeHeadlines event from the codeunits associated with the page (same ID and name as the page). Here you can compute your headlines. You should store the result in a table in your extension, so you can quickly get the results in step 3\. The computation is done in a background task, not to decrease the performance of the role center pages. +3. Subscribe to the OnIsAnyExtensionHeadlineVisible event from the page. This event is used to determine if any extension has visible headlines, and if so, hide the fallback headlines. You should set the ExtensionHeadlinesVisible variable to true if your extension has headlines to display at the time of the event. Otherwise, do nothing. +4. In the page, in the OnAfterGetRecord trigger, get the headline text and visibility values and copy them to your added fields. + +To format headlines, you should use Codeunit 1439 Headline Management functions: + +* Truncate: to truncate a text gracefully when possible with "...". For example, HeadlineManagement.Truncate('the text',6) returns "the...". +* Emphasize: to emphasize part of the headline payload. Emphasized text is shown with a different style. +* GetHeadlineText: to build the headline text. You provide the headline qualifier and payload, and you get the headline in a format that will be interpreted and formatted correctly by the client. It returns false if the qualifier exceeds its maximum length (50 characters) or payload exceeds its maximum length (75 characters). In that case it will not return the headline. If the qualifier you specify is empty, the default text "HEADLINE" will be displayed in the qualifier area. The payload must not be empty. + +### Examples: + +#### 1. Extending the page with a new headline: +```al +group(LargestSale) +{ + Visible = LargestSaleVisible; + ShowCaption=false; + Editable=false; + + field(LargestSaleText;LargestSaleText) + { + ApplicationArea = Basic, Suite; + DrillDown=true; + + trigger OnDrillDown() + var + EssentialBusHeadlineMgt: Codeunit "Essential Bus. Headline Mgt."; + begin + EssentialBusHeadlineMgt.OnDrillDownLargestSale(); + end; + } +} +``` + +#### 2. Subscribing to the OnComputeHeadlines event, and computing headlines +```al +[EventSubscriber(ObjectType::Codeunit, Codeunit::"Headline RC Business Manager", 'OnComputeHeadlines', '', true, true)] + procedure OnComputeHeadlinesBusinessManager() + begin + // [...] compute headline, and init the EssentialBusinessHeadline record + if not ShowHeadline then + exit; // not enough data to compute headline + + if not HeadlineManagement.GetHeadlineText( + 'Insight from last week', + StrSubstNo('The largest posted sales invoice was for %1', + HeadlineManagement.Emphasize(Format(CustomerLedgerEntry.Amount, 0, TypeHelper.GetAmountFormatWithUserLocale('$')))) + EssentialBusinessHeadline."Headline Text") + then + exit; + + EssentialBusinessHeadline.Validate("Headline Visible", true); + EssentialBusinessHeadline.Modify(); +end; +``` + +#### 3. Subscribing to the OnIsAnyExtensionHeadlineVisible event + +```al +[EventSubscriber(ObjectType::Page, Page::"Headline RC Business Manager", 'OnIsAnyExtensionHeadlineVisible', '', true, true)] +procedure OnIsAnyExtensionHeadlineVisible(var ExtensionHeadlinesVisible: Boolean) +var + EssentialBusinessHeadline: Record "Essential Business Headline"; + AtLeastOneHeadlineVisible: Boolean; +begin + EssentialBusinessHeadline.SetRange("Headline Visible", true); + EssentialBusinessHeadline.SetFilter("Headline Name",'%1|%2|%3|%4|%5', + EssentialBusinessHeadline."Headline Name"::LargestOrder, + EssentialBusinessHeadline."Headline Name"::LargestSale, + EssentialBusinessHeadline."Headline Name"::BusiestResource, + EssentialBusinessHeadline."Headline Name"::MostPopularItem, + EssentialBusinessHeadline."Headline Name"::SalesIncrease, + EssentialBusinessHeadline."Headline Name"::TopCustomer); + + AtLeastOneHeadlineVisible := not EssentialBusinessHeadline.IsEmpty(); + // only modify the var if this extension is making some headlines visible, setting to false could override some other extensions setting the value to true + if AtLeastOneHeadlineVisible then + ExtensionHeadlinesVisible := true; +end; +``` +#### + +#### 4. Setting the headline text on the page + +```al +trigger OnAfterGetRecord() +begin + EssentialBusinessHeadline.GetHeadline(EssentialBusinessHeadline."Headline Name"::LargestSale); + LargestSaleVisible := EssentialBusinessHeadline."Headline Visible"; + LargestSaleText := EssentialBusinessHeadline."Headline Text"; +end; +``` + +[![ ][image2]][anchor2] + +_Figure 2: Sequence diagram of headline usage_ + +## Usages in NAV: + +* Essential Business Headlines extension + + +[anchor0]: 3733.logo.png +[anchor1]: Headline.png +[anchor2]: 0724.Headline-sequence-diagram-v2.png + + +[image0]: 3733.logo.png +[image1]: Headline.png +[image2]: 0724.Headline-sequence-diagram-v2.png diff --git a/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/3058.Feature-localization-for-data-structures-3.png b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/3058.Feature-localization-for-data-structures-3.png new file mode 100644 index 00000000..8da249fe Binary files /dev/null and b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/3058.Feature-localization-for-data-structures-3.png differ diff --git a/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/5123.Feature-localization-for-data-structures-1.png b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/5123.Feature-localization-for-data-structures-1.png new file mode 100644 index 00000000..c63ae3f4 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/5123.Feature-localization-for-data-structures-1.png differ diff --git a/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/6052.Feature-localization-for-data-structures-2.png b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/6052.Feature-localization-for-data-structures-2.png new file mode 100644 index 00000000..7fca9371 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/6052.Feature-localization-for-data-structures-2.png differ diff --git a/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/index.md b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/index.md new file mode 100644 index 00000000..cd110eb1 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/feature-localization-for-data-structures/index.md @@ -0,0 +1,264 @@ ++++ +title = "Feature Localization For Data Structures" +weight = 570 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Bogdan Sturzoiu at Microsoft Development Center Copenhagen_ + +## Abstract + +This pattern shows a solution for integrating W1 features to pre-existing country features that use different tables to achieve similar functionality. + +## Description + +It sometimes happens that certain features are requested in a NAV-supported country, but they are not initially considered generic enough to be included in the W1 build. This is how local features, such as Subcontracting in Italy and India, were created or specific banking and payments functionality in Italy, France, Spain, and others. + +Then, at some point in time, a decision is made to create a W1 feature that is closely related to the local functionality but uses a completely different set of tables, pages, etc. The developers now face the following problem: How to enable the newly-developed W1 feature into a country, such that the customers who are accustomed to their local structures can seamlessly continue working without completely (or immediately) switching to the W1 objects. + +This was the issue that was tackled in the NAV 2013 R2, in relation to the SEPA Credit Transfers functionality. + +### Using a Proxy + +The generic Proxy pattern is "a class functioning as an interface to something else" ([Wikipedia][anchor0]). + +[![ ][image0]][anchor1] + +Figure 1\. Proxy in UML + +### Pattern Elements + +The NAV data model translation of the proxy pattern can be used as explained below. + +The RealSubject is the NAV data model. Variations in table structures, relationships, and numbers are particular to each country. The W1 model is the base for the country-localized data models. However, some countries have heavy localizations which cannot be directly processed by the W1 core objects. + +The proxy is a codeunit that gathers data from wherever it is stored and transforms it to fit into a standard table, which is later used across all localizations. + +The interface is the fixed form in which the data is presented to be consumed by the client. + +The client can be an XML port that is fed from the common data interface. It can also be any other data processor (a codeunit fed to another table, etc.) or data display object (page or report). + +### Pattern Steps + +1. The user creates records in the local tables. +2. The user invokes an action that must be processed using the W1 feature code. + +1. The proxy codeunit moves the data from the local tables to the W1 tables, either into a temporary or persistent set of records, as needed. + +1. The W1 code now performs the action on the W1 table data. + +## Usage + +In NAV 2013 R2, we released the SEPA Credit Transfer functionality. It involves exporting vendor payments to an XML file that is subsequently processed by the customer's bank. The payments are exported from the Payment Journal page through a configurable XMLport. Therefore, the data source for these payment lines is the Gen. Journal Line table (81). + +In various countries, we already had payment export functionality, usually into flat bank files. However, the files are generated from different tables than in W1\. For example, in Italy, vendor payments are handled through the Vendor Bill Header table (12181) and the Vendor Bill Line table (12182). They are the RealSubject. + +The W1 feature flow is as follows: + +[![ ][image1]][anchor2] + +Figure 2\. W1 object call sequence + +**Note:** CT = Credit Transfers, pain = payments initiation (the XML format used for SEPA Credit Transfers and SEPA Direct Debit). + +The key question is: Where to tap into this flow when using a local data structure. For this purpose, a proxy codeunit has been added in W1, called 1222 -- SEPA CT-Prepare Source. This codeunit feeds the client (XML1000) data in a standard format (the interface is the Gen. Journal Line table (81)). + +In W1, the codeunit simply outputs the same set of general journal lines that it receives as an input: + +```AL +OnRun(VAR Rec : Record "Gen. Journal Line") + +GenJnlLine.COPYFILTERS(Rec); + +CopyJnlLines(GenJnlLine,Rec); + +LOCAL CopyJnlLines(VAR FromGenJnlLine : Record "Gen. Journal Line";VAR TempGenJnlLine : TEMPORARY Record "Gen. Journal Line") + +IF FromGenJnlLine.FINDSET THEN BEGIN + +GenJnlBatch.GET(FromGenJnlLine."Journal Template Name",FromGenJnlLine."Journal Batch Name"); + +REPEAT + +TempGenJnlLine := FromGenJnlLine; + +TempGenJnlLine.INSERT; + +UNTIL FromGenJnlLine.NEXT = 0 + +END ELSE + +CreateTempJnlLines(FromGenJnlLine,TempGenJnlLine); + +LOCAL CreateTempJnlLines(VAR FromGenJnlLine : Record "Gen. Journal Line";VAR TempGenJnlLine : TEMPORARY Record "Gen. Journal Line") + +// To fill TempGenJnlLine from the source identified by filters set on FromGenJnlLine + +TempGenJnlLine := FromGenJnlLine; +``` + +In a country, such as Italy, the codeunit will have the following functions: + +1. Gets an empty set of general journal lines that carry the local payment document key as a filter on the Document No. field (as opposed to W1 that gets the real set of records to be exported). This is done so that the local data can be extracted at runtime. + +1. Selects the local payment data, for example in Italy, in the Vendor Bill Header and Vendor Bill Lines tables. + +1. Transforms the local payment data into temporary records of the Gen. Journal Line table. + +1. Outputs the temporary general journal lines that will be further processed and exported, exactly as in W1\. + +```AL +OnRun(VAR Rec : Record "Gen. Journal Line") + +GenJnlLine.COPYFILTERS(Rec); + +CopyJnlLines(GenJnlLine,Rec); + +LOCAL CopyJnlLines(VAR FromGenJnlLine : Record "Gen. Journal Line";VAR TempGenJnlLine : TEMPORARY Record "Gen. Journal Line") + +IF FromGenJnlLine.FINDSET THEN BEGIN + +GenJnlBatch.GET(FromGenJnlLine."Journal Template Name",FromGenJnlLine."Journal Batch Name"); + +REPEAT + +TempGenJnlLine := FromGenJnlLine; + +TempGenJnlLine.INSERT; + +UNTIL FromGenJnlLine.NEXT = 0 + +END ELSE + +CreateTempJnlLines(FromGenJnlLine,TempGenJnlLine); + +LOCAL CreateTempJnlLines(VAR FromGenJnlLine : Record "Gen. Journal Line";VAR TempGenJnlLine : TEMPORARY Record "Gen. Journal Line") + +PaymentDocNo := FromGenJnlLine.GETFILTER("Document No."); + +VendorBillHeader.GET(PaymentDocNo); + +VendorBillLine.RESET; + +VendorBillLine.SETCURRENTKEY("Vendor Bill List No.","Vendor No.","Due Date","Vendor Bank Acc. No.","Cumulative Transfers"); + +VendorBillLine.SETRANGE("Vendor Bill List No.",VendorBillHeader."No."); + +VendorBillLine.SETRANGE("Cumulative Transfers",TRUE); + +IF VendorBillLine.FINDSET THEN BEGIN + +CumulativeAmount := 0; + +PrevVendorBillLine := VendorBillLine; + +REPEAT + +VendorBillLine.TESTFIELD("Document Type",VendorBillLine."Document Type"::Invoice); + +IF ((VendorBillLine."Vendor No." <> PrevVendorBillLine."Vendor No.") OR (VendorBillLine."Vendor Bank Acc. No." <> PrevVendorBillLine."Vendor Bank Acc. No.")) THEN BEGIN InsertTempGenJnlLine(TempGenJnlLine,VendorBillHeader,PrevVendorBillLine,CumulativeAmount); + +CumulativeAmount := VendorBillLine."Amount to Pay"; + +END ELSE + +CumulativeAmount += VendorBillLine."Amount to Pay"; + +PrevVendorBillLine := VendorBillLine; + +UNTIL VendorBillLine.NEXT = 0; InsertTempGenJnlLine(TempGenJnlLine,VendorBillHeader,PrevVendorBillLine,CumulativeAmount); + +END; + +VendorBillLine.SETRANGE("Cumulative Transfers",FALSE); + +IF VendorBillLine.FINDSET THEN + +REPEAT + +VendorBillLine.TESTFIELD("Document Type",VendorBillLine."Document Type"::Invoice); InsertTempGenJnlLine(TempGenJnlLine,VendorBillHeader,VendorBillLine,VendorBillLine."Amount to Pay"); + +UNTIL VendorBillLine.NEXT = 0; + +LOCAL InsertTempGenJnlLine(VAR TempGenJnlLine : TEMPORARY Record "Gen. Journal Line";VendorBillHeader : Record "Vendor Bill Header";VendorBillLine : Record "Vendor Bill Line";AmountToPay : Decimal) + +WITH TempGenJnlLine DO BEGIN + +INIT; + +"Journal Template Name" := ''; + +"Journal Batch Name" := ''; + +"Document Type" := "Document Type"::Payment; + +"Document No." := VendorBillLine."Vendor Bill List No."; + +"Line No." := VendorBillLine."Line No."; + +"Account No." := VendorBillLine."Vendor No."; + +"Account Type" := TempGenJnlLine."Account Type"::Vendor; + +"Bal. Account Type" := TempGenJnlLine."Bal. Account Type"::"Bank Account"; + +"Bal. Account No." := VendorBillHeader."Bank Account No."; + +"Applies-to Ext. Doc. No." := VendorBillLine."External Document No."; + +Amount := AmountToPay; + +"Applies-to Doc. Type" := VendorBillLine."Document Type"; + +"Applies-to Doc. No." := VendorBillLine."Document No."; + +"Currency Code" := VendorBillHeader."Currency Code"; + +"Due Date" := VendorBillLine."Due Date"; + +"Posting Date" := VendorBillHeader."Posting Date"; + +"Recipient Bank Account" := VendorBillLine."Vendor Bank Acc. No."; + +Description := VendorBillLine.Description; + +"Message to Recipient" := VendorBillLine."Description 2"; + +INSERT; + +END; +``` + +The derived local feature flow is as follows: + +[![ ][image2]][anchor3] + +Figure 3\. The local country object flow + +As we can see from the diagram, this solution allows integration of the local and W1 features with a minimum amount of changes in W1 code. The only two differences are: + +1. The entry point of the flow is the local table/page. + +1. Codeunit 1222 is overloaded to prepare general journal lines from the local records. + +## NAV Usages + +The data mapping technique has been used for the SEPA Credit Transfer feature, and will be used in subsequent local integration projects. + +## Ideas for improvement + +A weak point for this pattern is the need to set a filter on the empty journal line in order to retrieve the local data when exporting from a local page. This can cause problems if the size of the local table document number is larger than the Document No. field (ID 20) in the Gen. Journal line table (81). + +Also, there is a strong need for thorough testing when using this pattern, because there might be differences in the behavior of the local table and table 81\. Whatever is acceptable for the local table may not be acceptable for the W1 table. A deep functional analysis is needed to see if the local export feature uses the same constraints as the W1 feature. + + + +[anchor0]: http://en.wikipedia.org/wiki/Proxy_pattern +[anchor1]: 5123.Feature-localization-for-data-structures-1.png +[anchor2]: 6052.Feature-localization-for-data-structures-2.png +[anchor3]: 3058.Feature-localization-for-data-structures-3.png + + +[image0]: 5123.Feature-localization-for-data-structures-1.png +[image1]: 6052.Feature-localization-for-data-structures-2.png +[image2]: 3058.Feature-localization-for-data-structures-3.png diff --git a/content/docs/NAVPatterns/patterns/hooks/5383.HookPattern1.png b/content/docs/NAVPatterns/patterns/hooks/5383.HookPattern1.png new file mode 100644 index 00000000..7e48236c Binary files /dev/null and b/content/docs/NAVPatterns/patterns/hooks/5383.HookPattern1.png differ diff --git a/content/docs/NAVPatterns/patterns/hooks/6378.HookPattern2.png b/content/docs/NAVPatterns/patterns/hooks/6378.HookPattern2.png new file mode 100644 index 00000000..333aced2 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/hooks/6378.HookPattern2.png differ diff --git a/content/docs/NAVPatterns/patterns/hooks/8156.HookPattern3.png b/content/docs/NAVPatterns/patterns/hooks/8156.HookPattern3.png new file mode 100644 index 00000000..acb15838 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/hooks/8156.HookPattern3.png differ diff --git a/content/docs/NAVPatterns/patterns/hooks/8875.HookPattern4.png b/content/docs/NAVPatterns/patterns/hooks/8875.HookPattern4.png new file mode 100644 index 00000000..4f14a725 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/hooks/8875.HookPattern4.png differ diff --git a/content/docs/NAVPatterns/patterns/hooks/index.md b/content/docs/NAVPatterns/patterns/hooks/index.md new file mode 100644 index 00000000..9134887d --- /dev/null +++ b/content/docs/NAVPatterns/patterns/hooks/index.md @@ -0,0 +1,104 @@ ++++ +title = "Hooks" +weight = 620 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By Eric Wauters ([waldo][anchor0]), Partner-Ready-Software_ + +## Abstract + +As a partner, adding new code to NAV means interfering with code shipped by Microsoft. Minimize your footprint of changes in Microsoft code, so that, when a new NAV version is shipped, you avoid conflicts and upgrade impact. The core NAV code is the "danger zone" - the less you touch it, the happier your upgrade will be. + +## Description + +When doing development over years, by different developers with different mindsets, the standard codebase gets changed a lot, adding multiple lines of code, adding local and global variants, adding or changing keys, changing existing business logic, ... . In other terms, the standard text objects are being changed all over the place.. . + +After years, it's not clear why a change was done, and what was the place where the change was intended to be done. And the latter is quite important in an upgrade process, when code in the base product is being refactored: if the exact place of the posting of the Customer Entry is being redesigned to a separate number, the first thing I need to know, is that I did a certain change at the place: "where the posting of the Customer Entry starts". The definition of that place, we call a "Hook". + +By minimizing the code in already existing application objects, you will make the upgrade process much easier, and all customization business logic will be grouped in new objects. When using atomic coding, it will be very readable what is being customized on a certain place in an existing part of the application. + +To minimize the impact of customizations, the idea of hooks is: + +* First of all, name the places in the already existing code where customization is needed; +* Second, place your business logic completely outside the already existing application code. + +I recommend to use this concept on: + +* All objects of the default applications that need to be changed +* On objects that should not hold any business logic (like tables, pages, XMLPorts) + +## Usage + +**Step 1** - if it doesn't exist yet - you create your Hook Codeunit. As the name assumes .. this is always a codeunit. We apply the following rules to it: + +* One Hook always hooks into one object. Which basically means that I will only declare this new codeunit in one other object (which is its parent object) +* The naming convention is: "The_Original_Object_Name Hook". Naming conventions are important, just to find your mapped object, and also to be able to group the Hooks. + +**Step 2**, you create the hook, which is basically a method (function) in your codeunit. The naming is important: + +* The naming of the hook should NOT describe what it is going to do (So, examples like "CheckMandatoryFields", "FillCustomFields" should not be used as a hook) +* The naming of the hook should describe WHERE the hook is placed, not what the hook will be doing (as nobody is able to look into the future .. :-)) +* To help with the naming, it is a good convention to use the "On"-prefix for these triggers. This way, it's very clear what are hooks, and what aren't.. + +**Step 3**, it's time to hook it to its corresponding object and right place in the business logic of that object. You do this by declaring your codeunit as a global in your object, and using the created hook function on its place in the business logic. This way, these one-liners apply: + +* A Hook Codeunit is only used once in one object only (its corresponding object) +* A Hook (function) is used only once in that object. As a consequence, changing the parameters has no consequence: you only need to change one function-call +* The codeunit is declared as a global. That exact global is the only custom declaration in the existing object .. Everything else is pushed to the hook-codeunit. + +**Step 4**, implement your business logic in the hook. Do this in the most atomic way, as there is a good chance that this same hook is going to be used for other business logic as well. Best is to use a one-line-function-call to business logic, so that the Hook Function itself stays readable. + +## Example + +Suppose, we want to add business logic just before posting a sales document. In that case, we have to look for the most relevant place, which is somewhere in the "Sales-Post" codeunit. So: + +**Step 1**: create codeunit "Sales-Post Hook" + +[![ ][image0]][anchor1] + +**Step 2**: create the hook function "OnBeforePostDocument" + +[![ ][image1]][anchor2] + +**Step 3**: declare a global in the "Sales-Post"-codeunit, called "SalesPostHook". Then, call the Hook Function that you created in Step 2 in the right place. + +[![ ][image2]][anchor3] + +**Step 4**: implement the business logic, by calling out to a new function. And implement the test-codeunit. + +[![ ][image3]][anchor4] + +## Consequences + +This pattern can be used in all cases to put business logic. But I see three possible approaches + +1. You only declare the most important and most used hooks + * This way, you limit the amount of objects and hooks to only a few in the default product + * For example, only the OnBeforePostSalesHeader, OnBeforeReleaseSalesDocument, .. And no field validation or such... +2. Only at objects of the default application which you are customizing. + * This way, you don't want to create hooks for your own objects, only default existing objects. +3. (recommended) You create hooks on all places you don't want to write business logic, and on all existing objects which you would like to customize. + * This is a very consistent way of working, as in any case, your business logic ends up in either a hook, or in its corresponding objects from a specific design pattern. But you know that the entry point is always a hook. + * You know what to expect in any case, both changed business logic in existing code and business logic in newly created code is entered from a hook. + +## Related Topics + +Atomic coding: It's important that the hook function is readable in the most extreme way. For this, we recommend to use the "Atomic Coding" concept. + +See here a comparison / extension of hooks : [http://www.waldo.be/2016/02/29/nav-2016-hooks-or-events/][anchor5] + + + +[anchor0]: http://www.waldo.be "waldo's blog" +[anchor1]: 5383.HookPattern1.png +[anchor2]: 6378.HookPattern2.png +[anchor3]: 8156.HookPattern3.png +[anchor4]: 8875.HookPattern4.png +[anchor5]: http://www.waldo.be/2016/02/29/nav-2016-hooks-or-events/ + + +[image0]: 5383.HookPattern1.png +[image1]: 6378.HookPattern2.png +[image2]: 8156.HookPattern3.png +[image3]: 8875.HookPattern4.png diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0333.Figure-3.PNG b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0333.Figure-3.PNG new file mode 100644 index 00000000..a5b92cbe Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0333.Figure-3.PNG differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0458.Figure-1.PNG b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0458.Figure-1.PNG new file mode 100644 index 00000000..ef41da07 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0458.Figure-1.PNG differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0552.Example-Figure-5.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0552.Example-Figure-5.png new file mode 100644 index 00000000..4f23c91a Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0552.Example-Figure-5.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0638.Figure-2.PNG b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0638.Figure-2.PNG new file mode 100644 index 00000000..d32b76a3 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/0638.Figure-2.PNG differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1488.Example-Figure-1.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1488.Example-Figure-1.png new file mode 100644 index 00000000..1b9271de Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1488.Example-Figure-1.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1884.Example-Figure-7.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1884.Example-Figure-7.png new file mode 100644 index 00000000..d03541ac Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/1884.Example-Figure-7.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/2068.Example-Figure-4.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/2068.Example-Figure-4.png new file mode 100644 index 00000000..023a947f Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/2068.Example-Figure-4.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4477.Example-Figure-6.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4477.Example-Figure-6.png new file mode 100644 index 00000000..a32c6b86 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4477.Example-Figure-6.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4532.Example-Figure-3.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4532.Example-Figure-3.png new file mode 100644 index 00000000..463d2358 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4532.Example-Figure-3.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4682.Example-Figure-2.png b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4682.Example-Figure-2.png new file mode 100644 index 00000000..4a95a938 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/4682.Example-Figure-2.png differ diff --git a/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/index.md b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/index.md new file mode 100644 index 00000000..a89b6b1b --- /dev/null +++ b/content/docs/NAVPatterns/patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/index.md @@ -0,0 +1,161 @@ ++++ +title = "Surrogate keys using Autoincrement Pattern" +weight = 630 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_By [Soren Klemmensen][anchor0], [_Partner-Ready-Software_ ][anchor1] & [360 Visibility][anchor2]_ + +## Abstract + +This Pattern is meant to create generic & reusable links between tables. The goal is to have an easy generic way to link a generically designed sub table to a record on a main table which can be used for other links too. + +To minimize the impact of customizations and to keep modules as generic and reusable as possible the idea of the Implementation of surrogate keys using AutoIncrement pattern is: + +* To create a generic and reusable auto generated link (A Surrogate Key), Immune to natural key data & requirement changes, on a main table with minimum impact on the table. +* To create generic and reusable sub tables that effortless can be reused anywhere in the application. + +## Description + +Over years of development many things are repeated across different implementation and even inside the same application. A typical example could be adding comments to an area just as it is done in Microsoft Dynamics NAV multiple times. There can be reasons for doing this again and again, but not only does this need to be maintained and upgraded over the years, but all the implementations of comments also needs to be tested separately. If a standard and generic comment could be developed and a generic way of connecting it to a main table this could be resolved. This is exactly what this pattern is trying accomplish. + +[![ ][image0]][anchor3] + +_Figure 1: Table structure for linking a Document Header and Line Table with a Document Comment Table._ + +[![ ][image1]][anchor4] + +_Figure 2: Table Structure for linking a Master Data Table with a Master Data Comment Table_ + +A typical way of linking a table to master data or to a document has been to use the primary key of the table being linked to. This causes some issues as the linked table now is designed specifically for the main table and it functionality cannot be reused. In case of renames the linked table needs to be renamed too which is costly in processing. Code also needs to be added on the delete trigger of the table to ensure that the attached records get removed if needed which increases the over all footprint of any change. + +[![ ][image2]][anchor5] + +_Figure 3: A Generic Way of creating a Comment table and linking it in a generic way to the main table no matter what this table might be. The Unique Record Identifier on the main tables is an Integer with AutoIncrement set to yes._ + +It is recommended using this pattern in all tables which need sub tables unless specific reasons exists for not doing this. + +## Usage + +**Step 1**: Create a generic Unique Record Identifier in the main table + +The Pattern is implemented by adding a Field (I have called it Unique Record Identifier for this article) in a table (the main Table) where links are needed to be established to. Set the Property Data Type to Integer, Editable to No & AutoIncrement to Yes. + +**Step 2**: Create a generic link-able sub table. + +Create a new table (Sub Table) which you would like to be reusable with a primary key containing + +* Table No. of Data Type Integer +* Unique Record Identifier of Data Type Integer +* A 3 field for allowing multiple entries to exist per record in the main table if so needed. This could be a Line No. of Data Type Integer or a Code field of Data Type Code(10) and so on. + +The important part here is that the 2 or 3 first fields in the tables primary key is the Table No. and the Unique Record Identifier. If a 3rd field was added to allow for multiple entries to exist per record in the main table this should also be included in the primary key. + +Make sure to also add any other fields containing the information you wish to store as needed to the table. + +The sub table will be immune to renames from the main table as the main tables primary key is not used in the primary key of the sub table. The Insert, Modify and Rename triggers are not causing any issues and only the delete trigger will need to be considered. This can be dealt with generically from the OnDatabaseDelete trigger in Codeunit 1\. I would recommend to use a Hook Pattern for this. + +**Step 3**: Create a page showing the sub table information. + +Now create a page showing the data in the sub table. + +**Step 4**: Add an Action or factbox. + +Create an action or factbox on the pages showing the main table data linking to the subpage with Table ID filtered to a constant of the Table No. of the main table and the Unique Record Identifier of the sub table filtering to the Unique Record Identifier of the main table. + +**Step 5**: Create a Hook for Function OnDatabaseDelete in Codeunit 1 ApplicationManagement if one doesn't already exist. See the Hook pattern. + +**Step 6**: Create a code to delete records in the Sub table if a main table record is deleted if this is required. This function needs to be called from the Hook created in Step 5\. + +## **NAV Specific Example** + +Let's assume we would like to create comment for a larger number of very different tables in the system. + +**Update the Customer table (Step 1)**: Go to table 18 Customer and add Field 50000 Unique Record Identifier. Set the Property Data Type to Integer, Editable to No & AutoIncrement to Yes. Save the changes. + +[![ ][image3]][anchor6] + +_[![ ][image4]][anchor7] + + +**Create Comment table (Step 2)**: Create a new table called Comment. Add 3 fields Table No., Unique Record Identifier & Line No. all of Data Type Integer. Make these 3 fields into the primary key for the table. Add a 4 field called Comment with Data Type Text(80). Save the changes. + +[![ ][image5]][anchor8] + +**Create the Comments page (Step 3)**: Start the page wizard and create a Page based on table comment created above as a List only showing the Comment field. Set AutoSplitKey property to Yes on the page. + +_[![ ][image6]][anchor9]_ + +**Update the Customer Card (Step 4)**: Add an action to the Customer Card to open the Comments. Go to Actions and create an action called Smart Comment. Populate the properties RunObject, RunPageView and RunPageLink as see in the picture below. + +[![ ][image7]][anchor10] + +**Create a hook OnDatabaseDelete (Step 5)**: + +Create a codeunit 50000 called "ApplicationManagement Hook" with one function called OnAfterOnDatabaseDelete taking the parameter RecRef of Data Type RecordRef. Add it as a global variable to Codeunit 1 ApplicationManagement and call the function as the last line in OnDatabaseDelete. Please read about the hook pattern before implementing it. + +[![ ][image8]][anchor11] + +**Create the code needed to delete comments linked to a deleted record (Step6)**: + +Create a DeleteComments function taking the RecRef of Data Type RecordRef and add the code as shown blow. + +[![ ][image9]][anchor12] + +It is assumed in this example for simplicity that the Field 50000 is reserved across the entire application for the Unique Record Identifier as defined in Step 1\. + +The comments are now fully working if we look away from the fact that we did not deal with a few things specific to Sales orders like RecreateSalesLines function, Archiving and Copy Document. All of which can easily be dealt with. + +These comments are now completely reusable everywhere else in the system. Sales Document is a perfect example as the primary keys of the Sales Header and the Sales line are both different from the customer and normally we would not be able to use the comments header or the line. All that needs to be done is adding the Field 50000 Unique Record Identifier to the Sales Header & Sales Line (Disregarding the posted documents in this example) and creating the actions on the Page Actions with the needed filters. Deletion is already handled of comments no matter what the main table might be. + +Looking at the main table the Unique Record Identifier is also completely reusable for any other linking needed. + +Other ideas for use of this pattern could be. An Error table, Tags, Dynamically defined fields and Generic fact boxes. Only the imagination sets limits for its use. + +Upgrade wise there can be an impact if data is moved out of tables to be reinserted again because of a change to a database structure. This will cause the Unique Record Identifier to change, unless steps are taken to avoid this, and the links will need to be reestablished. + +Other risks could be if Transfer Fields are used and data is being copied unintentionally. + +## NAV Usages + +This is a new pattern not currently used in Microsoft Dynamics NAV. + +## Ideas for improvement + +The reason for doing it this way is that you now can reuse your code again and again with only creating the same link on other tables instead of building it from scratch each time reducing testing needed and improving the quality of the overall product. + +## Consequences (When it should not be used) + +This can be used on any table when linking anything to it that can be considered a generic module which can be reused. That said it should not be used in cases where there is a risk of tables growing so much that performance could be impacted. There are ways to reduce performance impact when using high volume tables, but that is outside the scope of this pattern. + +## Related Topics + +This is related to the hook pattern as far as they both try to reduce the footprint changes have on the standard application, by creating reusable ways to interact with the standard code. That said the hook pattern is more about hooking the functionality to existing while this pattern is more about creating reusable ways of creating functionality. + + + +[anchor0]: http://mvp.microsoft.com/en-us/mvp/Soren%20Klemmensen-5001002 "Soren Klemmensen" +[anchor1]: http://partner-ready-software.com/ "Partner-Ready-Software" +[anchor2]: http://www.360visibility.com/ "360 Visibility" +[anchor3]: 0458.Figure-1.PNG +[anchor4]: 0638.Figure-2.PNG +[anchor5]: 0333.Figure-3.PNG +[anchor6]: 1488.Example-Figure-1.png +[anchor7]: 4682.Example-Figure-2.png +[anchor8]: 4532.Example-Figure-3.png +[anchor9]: 2068.Example-Figure-4.png +[anchor10]: /cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-42/0552.Example-Figure-5.png +[anchor11]: /cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-42/4477.Example-Figure-6.png +[anchor12]: /cfs-file.ashx/__key/communityserver-wikis-components-files/00-00-00-00-42/1884.Example-Figure-7.png + + +[image0]: 0458.Figure-1.PNG +[image1]: 0638.Figure-2.PNG +[image2]: 0333.Figure-3.PNG +[image3]: 1488.Example-Figure-1.png +[image4]: 4682.Example-Figure-2.png +[image5]: 4532.Example-Figure-3.png +[image6]: 2068.Example-Figure-4.png +[image7]: 0552.Example-Figure-5.png +[image8]: 4477.Example-Figure-6.png +[image9]: 1884.Example-Figure-7.png diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/2804.Picture-2.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/2804.Picture-2.png new file mode 100644 index 00000000..d6df77d4 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/2804.Picture-2.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/5707.Picture-4.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/5707.Picture-4.png new file mode 100644 index 00000000..5976b18c Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/5707.Picture-4.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6215.picture-1.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6215.picture-1.png new file mode 100644 index 00000000..cf794efa Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6215.picture-1.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6685.picture-3.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6685.picture-3.png new file mode 100644 index 00000000..f75ce0c1 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/6685.picture-3.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7217.picture-1.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7217.picture-1.png new file mode 100644 index 00000000..9f15bb4e Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7217.picture-1.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7245.Picture-2.png b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7245.Picture-2.png new file mode 100644 index 00000000..d0184198 Binary files /dev/null and b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/7245.Picture-2.png differ diff --git a/content/docs/NAVPatterns/patterns/instructions-in-the-ui/index.md b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/index.md new file mode 100644 index 00000000..fdbe7999 --- /dev/null +++ b/content/docs/NAVPatterns/patterns/instructions-in-the-ui/index.md @@ -0,0 +1,167 @@ ++++ +title = "Instructions in the UI" +weight = 670 +tags = ["C/AL"] +categories = ["Pattern"] ++++ +_Originally by Nikola Kukrika at Microsoft Development Center Copenhagen_ + +## Abstract + +To mitigate usability problems with learnability or discoverability of NAV functionality, it is possible to embed instructions in the UI in connection with the task that the user is performing. The goal is to explain how to use the product or feature without impairing the user's productivity after user has learned how to use a feature. + +## Description + +Users must often go through a few days of training to learn how to use NAV, and even then, many users rely on super users to help them mitigate difficulties using NAV. In addition, because of low discoverability and learnability, many useful features are not being used at all. + +Users' expectations are changing. They expect the software to be usable out-of-the-box because this is the trend in software generally. + +One of the cheapest and most effective methods to solve usability issues is to embed instructional messages in the product. From a user-experience point of view, **this should be used as a last resort**. UI should be self-explanatory, efficient, and simple to use. Accordingly, you should only implement this pattern if simplifying and improving a scenario is not possible or is too expensive. + +In this connection, the most important requirement is not to impair productivity of the users. One of the biggest and most common UX mistakes that developers make is to "optimize for new users". After the user has learned how to use the product, all the instruction texts and dialogs that we added to the UI will clutter the page and make information less visible. Instructional dialogs on routine tasks will become annoying. Therefore, we must make all instructions dismissible. + +In the Mini App solution we have used following elements: + +1. Dismissible dialogs +2. FastTabs with instructional text +3. Help tiles on a Role Center +4. Tooltips on actions and fields +5. Task-oriented page Help + +## Usage + +The following pattern applies to dismissible parts in the UI. + +We have a table that stores the instructional code ID and the UserID, so that we can track which user has turned off which instruction. All the logic handling is done from a codeunit. It is the responsibility of the codeunit to show/hide dialogs if needed. + +[![ ][image0]][anchor0] + +## Dismissible Dialogs + +Dismissible dialogs show the instructional message about the functionality, with the user option to select "Don't show this again". This is a good solution to problems where users enter text in the wrong place, or to explain behavior of a somewhat hidden feature. + +[![ ][image1]][anchor1] + +On a recent usability study of the **Description** field on sales or purchase lines, most users ignored the **No.** field and started entering text in the description field before proceeded to enter a quantity. In the solution in question, text only is treated as a line comment if the **No.** field is blank. The fix was to update the field name to **Description/Comment** and to provide a message that typing in the field creates a comment only. Users that often use comments can then choose "Do not show again" to get rid of the instructional text. + +**When to use:** Recommendation is to use only when many users are entering data in the wrong way and modifying the code is costly. This is an interrupting dialog, but the benefits are that it is very hard to overlook this dialog. + +## FastTabs with Instructional Text + +Instructional text on FastTabs is ideal for showing larger amounts of text in the UI. + +[![ ][image2]][anchor2] + +When a user changes a filter in the **Navigate** page, we blank the grid. This may confuse the users as to how to proceed. The **Notification** FastTab provides information on what has happened and gives instructions on how to proceed. + +Similar implementation is to have the FastTab always visible with the **Do not show again** check box present, so that users can dismiss it after they have read the message. This is less intrusive than using a dialog, and it has the benefit of being always visible compared to the dialog. The drawback is that users may not read it or may not dismiss it. + +## Information Tiles on Role Centers + +On the **Small Business Role Center** page (9022), we have implemented a **Getting Started** group containing action tiles. Clicking on the first two tiles will play instructional videos. Clicking on the third tile launches a dedicated help topic. Since these tiles will get in the way of the experienced users, an option to hide the entire group is provided. + +[![ ][image3]][anchor3] + +**Note**: in NAV 2013 R2, actions appear as tiles in the web client only. In the win client, they appear as links. In the web client, the actions only appear if they are in a group for themselves (without Stack Queues -- empty group with only actins defined). + +### To implement tiles for instructional videos + +1. Upload a video to a video hosting service (check if licensing is allowing you to use if for this usage. Examples of video hosting services are: YouTube, Vimeo, Yahoo Video.) +2. Get the code to embed the video (select option embed). +3. You can reuse the code on the **Mini Video Player Page** page (1395) or implement a custom one. + +Important parts: +**VideoPlayerAddIn.SetFrameAttribute** function is used to set an attribute to the iframe that will be playing the video. + + +Example of the embed code: + +```js +