PowerShell: C/AL Objektpaketkonfigurator

22. Januar 2018 01:58

An diesem Wochenende ist mein bislang aufwendigstes Skript in seiner ersten Version aus der Taufe gehoben worden :-) . Mein Objektsplitter letzte Woche war ja eigentlich "nur" eine kleine Vorübung dazu, der werkelt hier auch mit, die Quellobjekte werden analog wie dort zerlegt.

Wenn man aus einer als Textdatei vorliegendem C/AL-Objektpaket von oftmals Hunderten oder mehr Objekten nur einige benötigt, die dort als ein neues Paket herausgelöst werden müssen (bspw. für Abgleichszwecke), ist das normalerweise eine fummelige Aufgabe, aber nun nicht mehr :wink: .

Mit diesem Skript kann man das Objektpaket einlesen, sich die daraus benötigten Objekte in einer Treeview zusammenklicken und die Auswahl dann als eine neue Objektpaketdatei exportieren.

Bedienung
Skript starten, automatisch erscheint das Fenster um das Quellpaket auswählen.
opkonf1.png

Das Quellpaket wird zerlegt. Wenn dieses abgeschlossen ist, wird die Hauptform erzeugt. In den Vordergrund kommt sie dabei, wenn das Skript im ISE läuft, nicht automatisch, diese hängt dann am PowerShell ISE-Icon in der Taskleiste. Bei Ausführung über die PowerShell-Konsole poppt das Fenster dagegen hoch.
Hier die benötigten Objekte in der Treeview zusammenklicken, der Inhalt des Objekts, für das aktuell ein Haken gesetzt wurde, wird jeweils in der Textbox angezeigt.
Entfernt man den Haken wieder, wird auch der Inhalt der Textbox gelöscht, aber die zugrundeliegende Objektdatei natürlich nicht :mrgreen:. In der Treeview kann man beliebig oft mit den Haken in den Checkboxen hin und her klicken, bis man mit dem Ergebnis zufrieden ist. Dann oben links auf "Create Object Package" klicken.
opkonf2.png

Den Namen des neuen Objektpakets angeben.
opkonf3.png

Die ausgewählten Objekte werden zusammengestellt und als neues Objektpaket exportiert.
opkonf4.png

Dann die Form schließen, das Skript erneut starten und man kann sich durch Öffnen der gerade gespeicherten Datei das erzeugte Objektpaket anschauen 8-) .
opkonf5.png


Es können beliebig viele neue Pakete aus einem Quellpaket erzeugt werden, für weitere Pakete einfach die Haken neu setzen und noch einmal auf "Create Object Package" und einen anderen Dateinamen wählen.

GUI braucht noch etwas Feintuning, aber der wichtigere Teil des genauen Textexports in unveränderter Objektreihenfolge :!: läuft schon.

Code:
<#
        .SYNOPSIS
        Object Package Viewer and Configurator
        .DESCRIPTION
        This script opens a NAV Object Package, displays it in a treeview and offers multiple selection of single objects
        to create a new object package
        .NOTES
        File Name  : NAVObjectPackageConfigurator.ps1
        Author     : Kai Kowalewski
        Requires   : PowerShell V3
        .LINK
   
        .EXAMPLE

#>
function NAVObjectPackageConfigurator
{
    # Install .Net Assemblies
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
    $ErrorActionPreference = "Stop"
    $PSDefaultParameterValues['*:ErrorAction']='Stop'

    $initialDirectory = 'C:\temp'

   
    [Windows.Forms.Application]::EnableVisualStyles()
    [reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
    [reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
    [reflection.assembly]::loadwithpartialname("System.Windows.Forms.SaveFileDialog") | Out-Null
    $sourceEncoding = [System.Text.Encoding]::GetEncoding(850)

   

    $dt = New-Object System.Data.Datatable
    [void]$dt.Columns.Add("ObjectIndex")
    [void]$dt.Columns.Add("ObjectFileName")
    $dt.Columns["ObjectIndex"].Datatype = [string]
    $dt.Columns["ObjectFileName"].DataType = [string]
    [void]$dt.clear
 
   

    function Read-SingleObjectFile
    {
        param (
            $SingleObjectFilename
        )
       
        $richTextBox1.text = [System.IO.File]::ReadAllText("$WorkingFolder\SPLITNAVOBJ\$SingleObjectFileName", $sourceencoding)   
         
        $form1.refresh()
    }

    function Clear-ObjectContent
    {   
        $richTextBox1.text = ""
        $form1.refresh()
    }
   
    function Concat-SelectedObjectFiles
    {

        Add-Type -AssemblyName System.Windows.Forms
        $SelectedTabCnt = 0
        $SelectedPagCnt = 0
        $SelectedRepCnt = 0
        $SelectedCodCnt = 0
        $SelectedXmlCnt = 0
        $SelectedForCnt = 0
        $SelectedDatCnt = 0
        $SelectedQueCnt = 0
        $SelectedMenCnt = 0
        $outputfile = Get-SaveFile "$Workingfolder"
        if ($outputfile -eq "") {throw 'Please select a file name for new object package'}

        [int]$SelObjCnt = 0
        if (test-path $outputfile) {Remove-Item $outputfile -force}

        $dw = New-Object System.Data.DataView($dt)
        $dw.Sort="ObjectIndex"
        ForEach ($DataRowView in $dw)

        {
            $CurrIndex = $($DataRowView[0])
            $CurrFile = $($DataRowView[1])
            [String]$SelectedObjectChar = $DataRowView[1].Substring(0,1)

            Switch ($SelectedObjectChar)
            {
                'T' {$SelectedTabCnt++}
                'P' {$SelectedPagCnt++}
                'R' {$SelectedRepCnt++}
                'C' {$SelectedCodCnt++}
                'X' {$SelectedXmlCnt++}
                'F' {$SelectedForCnt++}
                'D' {$SelectedDatCnt++}
                'Q' {$SelectedQueCnt++}
                'M' {$SelectedMenCnt++}
       
            }

            #Write-Host "$CurrIndex $CurrFile"
            $SelObjCnt++
            Add-Content -path $outputfile -value(Get-Content $WorkingFolder\SPLITNAVOBJ\$CurrFile) 
        }
        [string]$SelectedObjectList = ""
        if ($SelectedTABCnt -gt 0) {$SelectedObjectList += "TAB:$SelectedTABCnt "}
        if ($SelectedFORCnt -gt 0) {$SelectedObjectList += "FOR:$SelectedFORCnt "}
        if ($SelectedPAGCnt -gt 0) {$SelectedObjectList += "PAG:$SelectedPAGCnt "}
        if ($SelectedREPCnt -gt 0) {$SelectedObjectList += "REP:$SelectedREPCnt "}
        if ($SelectedDATCnt -gt 0) {$SelectedObjectList += "DAT:$SelectedDATCnt "}
        if ($SelectedCODCnt -gt 0) {$SelectedObjectList += "COD:$SelectedCODCnt "}
        if ($SelectedQUECnt -gt 0) {$SelectedObjectList += "QUE:$SelectedQUECnt "}
        if ($SelectedXMLCnt -gt 0) {$SelectedObjectList += "XML:$SelectedXMLCnt "}
        if ($SelectedMENCnt -gt 0) {$SelectedObjectList += "MEN:$SelectedMENCnt "} 
        [System.Windows.Forms.MessageBox]::Show("File $outputfile with $SelObjCnt objects ($SelectedObjectList) created.")
    }
    $button1_OnClick=
    {
        Concat-SelectedObjectFiles
 
    }

    $form1 = New-Object System.Windows.Forms.Form
    $linkLabel1 = New-Object System.Windows.Forms.LinkLabel
    $label4 = New-Object System.Windows.Forms.Label
    $label3 = New-Object System.Windows.Forms.Label
    $label2 = New-Object System.Windows.Forms.Label
    $button1 = New-Object System.Windows.Forms.Button
    $richTextBox1 = New-Object System.Windows.Forms.RichTextBox



    $linkLabel1_OpenLink=
    {
        [system.Diagnostics.Process]::start($linkLabel1.text)
    }

    Function Get-SaveFile($initialDirectory)
    {
        [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") |
        Out-Null

        $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog
        $SaveFileDialog.Title = 'Save new NAV object package'
        $SaveFileDialog.initialDirectory = $initialDirectory
       
        #$SaveFileDialog.filename = 'SelectedObjects.txt'
        $SaveFileDialog.filter = "NAV Object Files (*.txt)|*.txt"
        $SaveFileDialog.ShowDialog() | Out-Null
        $SaveFileDialog.filename
    }
   
    Function Get-FileName($initialDirectory)
    {
        [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
   
        $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
        $OpenFileDialog.initialDirectory = $initialDirectory
        $OpenFileDialog.filter = "NAV Object Files (*.txt)|*.txt"
        $OpenFileDialog.ShowDialog() | Out-Null
        $OpenFileDialog.filename
    }
       
   
   
    $inputfile = Get-FileName -initialDirectory "$env:HOMEDRIVE\temp" # This is the default path in OpenFile window.
    if ($inputfile -eq "") {throw 'Please select a file'}
     
    [decimal]$filesize = ((Get-Item $inputfile).length/1MB)
    $filesize =[math]::round($filesize,2)
    $inputfile = resolve-path $inputfile
    $WorkingFolder = Split-Path -Parent $inputfile

    #$form = New-Object System.Windows.Forms.Form

    $treeView1 = New-Object System.Windows.Forms.TreeView
    $newNode = new-object System.Windows.Forms.TreeNode
    $treeView1.Dock = 'Left'

    $treeView1.CheckBoxes = $true
   

   
    if (Test-path "$WorkingFolder\SPLITNAVOBJ\")
    {Remove-Item -path "$WorkingFolder\SPLITNAVOBJ\" -Recurse -Force}

    Write-Host "Splitting NAV objects from $inputfile (Size: $filesize MB) to $WorkingFolder\SPLITNAVOBJ\, this may take a while..." -ForegroundColor Yellow
    $startime = Get-Date
   
    $Sr = new-object System.IO.StreamReader($inputfile,[system.text.encoding]::GetEncoding(850))
    $ObjectLine = @{}
    $ObjectType = @{}
    $ObjectID = @{}
    $ObjectName = @{}

    [int]$ObjCnt = 0
    [int]$TabCnt = 0
    [int]$PagCnt = 0
    [int]$RepCnt = 0
    [int]$CodCnt = 0
    [int]$XmlCnt = 0
    [int]$ForCnt = 0
    [int]$DatCnt = 0
    [int]$QueCnt = 0
    [int]$MenCnt = 0
   


    while (-not $Sr.EndOfStream)
    {
        $Currline = $sr.ReadLine()
        if ($Currline.StartsWith('OBJECT'))
        {
            $TotalLineCnt++

            [String]$ObjectChar = $Currline.Substring(7,1)
            $ObjectLine = $currline.Split(' ')
            $ObjCnt++
            Switch ($ObjectChar)
            {
                'T' {$TabCnt++}
                'P' {$PagCnt++}
                'R' {$RepCnt++}
                'C' {$CodCnt++}
                'X' {$XmlCnt++}
                'F' {$ForCnt++}
                'D' {$DatCnt++}
                'Q' {$QueCnt++}
                'M' {$MenCnt++}
       
            }
            $ObjectFileName = $ObjectLine[1].Substring(0,3).ToUpper() + $ObjectLine[2] + ".TXT"
            $ObjectFullName = $CurrLine.Substring(7)
            #$ObjectLine.Add($TotalLineCnt,$TotalLineCnt)
            $ObjectType.Add($ObjCnt,$ObjectLine[1])
            $ObjectID.Add($ObjCnt,$ObjectLine[2])
            $ObjectName.Add($ObjCnt,$ObjectFullName)
            $newNode = new-object System.Windows.Forms.TreeNode
            $newNode.Name = $ObjectFileName
            $newNode.Text = $ObjectFullName
            $newNode.Tag =  $ObjCnt
           
           
            $treeView1.Nodes.Add($newNode) | Out-Null

            $Objectfile = New-Item -path "$WorkingFolder\SPLITNAVOBJ\$ObjectFileName" -type file -force
            IF (Test-Path $ObjectFile) {Remove-Item $ObjectFile}
           
            $sw = new-object System.IO.Streamwriter($Objectfile,$false,[system.text.encoding]::GetEncoding(850))
       
        }


       
        if (-not $Currline.StartsWith('}'))
        {$sw.writeline($Currline)}
        else
        {
            $sw.writeline($Currline)
            $sw.writeline()
            $sw.Flush()

        }

       
    }
   
    $endtime = Get-Date
    $time = $endtime - $startime
    Write-Host "$ObjCnt NAV objects splitted to $WorkingFolder\SPLITNAVOBJ\ in $($time.Minutes)m:$($time.Seconds)s:$($time.Milliseconds)ms" -ForegroundColor Yellow
    Write-Host "Tables: $TabCnt" -ForegroundColor Yellow
    Write-Host "Pages: $PagCnt" -ForegroundColor Yellow
    Write-Host "Reports: $RepCnt" -ForegroundColor Yellow
    Write-Host "Codeunits: $CodCnt" -ForegroundColor Yellow
    Write-Host "XMLPorts: $XMLCnt" -ForegroundColor Yellow
    Write-Host "Queries: $QueCnt" -ForegroundColor Yellow
    Write-Host "MenuSuites: $MenCnt" -ForegroundColor Yellow
    if ($ForCnt -gt 0)
    {Write-Host "Forms: $ForCnt" -ForegroundColor Yellow}
    if ($DatCnt -gt 0)
    {Write-Host "Dataports: $DatCnt" -ForegroundColor Yellow}
    [string]$ObjectList = ''
    if ($TABCnt -gt 0) {$ObjectList += "TAB:$TABCnt "}
    if ($FORCnt -gt 0) {$ObjectList += "FOR:$FORCnt "}
    if ($PAGCnt -gt 0) {$ObjectList += "PAG:$PAGCnt "}
    if ($REPCnt -gt 0) {$ObjectList += "REP:$REPCnt "}
    if ($DATCnt -gt 0) {$ObjectList += "DAT:$DATCnt "}
    if ($CODCnt -gt 0) {$ObjectList += "COD:$CODCnt "}
    if ($QUECnt -gt 0) {$ObjectList += "QUE:$QUECnt "}
    if ($XMLCnt -gt 0) {$ObjectList += "XML:$XMLCnt "}
    if ($MENCnt -gt 0) {$ObjectList += "MEN:$MENCnt "} 
    $sr.Close()
    $sr.Dispose()
    $sw.close()
    $sw.Dispose()

    $TV_AfterCheck =
    {
        if($_.Node.Checked)
        {
 
            $_.Node.Setfocus
            #$_.Node.SetFont = New-Object System.Drawing.Font("Microsoft Sans Serif",9,1,3,0)

            $row = $dt.NewRow() 
            [String]$TempIndex = $_.Node.Tag.ToString("0000000000")
            $row["ObjectIndex"] = $TempIndex
            $row["ObjectFileName"] = $_.Node.Name
            $dt.Rows.Add($row)
            $dt.AcceptChanges()
           
           
            #[void]$dt.Rows.Add("$_.Node.Tag","$_.Node.Name")
            Read-SingleObjectFile($_.Node.Name)
            #[System.Windows.Forms.MessageBox]::Show('Node ' + $_.Node.Text + ' checked')
        }
        else
        {

            [String]$TempIndex = $_.Node.Tag.ToString("0000000000")
            #[System.Windows.Forms.MessageBox]::Show('Node ' + $TempIndex + ' uncheckêd')
            $rows = $dt.Select("ObjectIndex = '$TempIndex'").Delete()
            foreach($row2 in $rows)
            {
                $row2.Delete()
            }
           
            $dt.AcceptChanges
            Clear-ObjectContent
            #[System.Windows.Forms.MessageBox]::Show('Node ' + $_.Node.Text + ' unchecked')
        }
    }

    $treeView1.Add_AfterCheck($TV_AfterCheck)

    $form1.text = 'NAV Object Package Configurator ' + [System.IO.Path]::GetFileName($inputfile) +  ' ' + 'Objects:' + $ObjCnt +  ' ' + $ObjectList
    $form1.Name = "form1"
    $form1.DataBindings.DefaultDataSourceUpdateMode = 0
    $System_Drawing_Size = New-Object System.Drawing.Size
    $System_Drawing_Size.Width = 838
    $System_Drawing_Size.Height = 612

    $form1.ClientSize = $System_Drawing_Size

 

    $linkLabel1.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",9,0,3,0)
    $System_Drawing_Size = New-Object System.Drawing.Size
    $System_Drawing_Size.Width = 155
    $System_Drawing_Size.Height = 23
    $linkLabel1.Size = $System_Drawing_Size
    $linkLabel1.TabIndex = 10
    $linkLabel1.Text = "http://www.msdynamics.de"
    $System_Drawing_Point = New-Object System.Drawing.Point
    $System_Drawing_Point.X = 180
    $System_Drawing_Point.Y = 0
    $linkLabel1.Location = $System_Drawing_Point
    $linkLabel1.TabStop = $True
    $linkLabel1.DataBindings.DefaultDataSourceUpdateMode = 0
    $linkLabel1.Name = "linkLabel1"
    $linkLabel1.add_click($linkLabel1_OpenLink)
 
    $form1.Controls.Add($linkLabel1)

    $button1.TabIndex = 4
    $button1.Name = "button1"
    $System_Drawing_Size = New-Object System.Drawing.Size
    $System_Drawing_Size.Width = 150
    $System_Drawing_Size.Height = 23
    $button1.Size = $System_Drawing_Size
    $button1.UseVisualStyleBackColor = $True
 
    $button1.Text = "Create Object Package"
 
    $System_Drawing_Point = New-Object System.Drawing.Point
    $System_Drawing_Point.X = 0
    $System_Drawing_Point.Y = 0
    $button1.Location = $System_Drawing_Point
    $button1.DataBindings.DefaultDataSourceUpdateMode = 0
    $button1.add_Click($button1_OnClick)
 
    $form1.Controls.Add($button1)
   
 
    <#
            $label3.TabIndex = 6
            $System_Drawing_Size = New-Object System.Drawing.Size
            $System_Drawing_Size.Width = 100
            $System_Drawing_Size.Height = 23
            $label3.Size = $System_Drawing_Size
            $label3.Text = "Object Content"
            $label3.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",9,0,3,0)
 
            $System_Drawing_Point = New-Object System.Drawing.Point
            $System_Drawing_Point.X = 255
            $System_Drawing_Point.Y = 3
            $label3.Location = $System_Drawing_Point
            $label3.DataBindings.DefaultDataSourceUpdateMode = 0
            $label3.Name = "label3"
 
            $form1.Controls.Add($label3)
    #>
    <#
            $label2.TabIndex = 5
            $System_Drawing_Size = New-Object System.Drawing.Size
            $System_Drawing_Size.Width = 100
            $System_Drawing_Size.Height = 12
            $label2.Size = $System_Drawing_Size
            $label2.Text = "Object List"
            $label2.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",8,0,3,0)
 
            $System_Drawing_Point = New-Object System.Drawing.Point
            $System_Drawing_Point.X = 8
            $System_Drawing_Point.Y = 8
            $label2.Location = $System_Drawing_Point
            $label2.DataBindings.DefaultDataSourceUpdateMode = 0
            $label2.Name = "label2"
 
            $form1.Controls.Add($label2)
    #>
 
    $richTextBox1.Name = "richTextBox1"
    $richTextBox1.Text = ""
    $richTextBox1.DataBindings.DefaultDataSourceUpdateMode = 0
    $System_Drawing_Point = New-Object System.Drawing.Point
    $System_Drawing_Point.X = 255
    $System_Drawing_Point.Y = 30
    $richTextBox1.Location = $System_Drawing_Point
    $System_Drawing_Size = New-Object System.Drawing.Size
    $System_Drawing_Size.Width = 800
    $System_Drawing_Size.Height = 454
    $richTextBox1.Size = $System_Drawing_Size
    $richTextBox1.Font = New-Object System.Drawing.Font("Courier New",11,0,3,0)
    $richTextBox1.WordWrap = $false
    $richtextbox1.ScrollBars = 'ForcedBoth'
    $richtextbox1.Dock = 'Fill'
    $richtextbox1.ReadOnly = $true
    $RichTextBox1.BackColor = [Drawing.Color]::White
    $RichTextBox1.ForeColor = [Drawing.Color]::Black
    $richTextBox1.TabIndex = 1
 
    $form1.Controls.Add($richTextBox1)

    $System_Drawing_Size = New-Object System.Drawing.Size
    $System_Drawing_Size.Width = 300
    $System_Drawing_Size.Height = 563
    $treeView1.Size = $System_Drawing_Size
    $treeView1.Name = "treeView1"
    $System_Drawing_Point = New-Object System.Drawing.Point
    $System_Drawing_Point.X = 100
    $System_Drawing_Point.Y = 100
    $treeView1.Location = $System_Drawing_Point
    $treeView1.DataBindings.DefaultDataSourceUpdateMode = 0
    $treeView1.TabIndex = 0
    $treeView1.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",10,0,3,0)
    $form1.Controls.Add($treeView1)
   
    #Save the initial state of the form
    #$InitialFormWindowState = $form1.WindowState
    # Menu Options - File / Exit
    $menuExit = New-Object System.Windows.Forms.ToolStripMenuItem
    $menuFile = New-Object System.Windows.Forms.ToolStripMenuItem
    #$menuExit.Image = [System.IconExtractor]::Extract(&quot;shell32.dll&quot;, 10, $true)
    $menuExit.ShortcutKeys = "Control, X"
    $menuExit.Text = "Exit"
    $menuExit.Add_Click({$form1.Close()})
    [void]$menuFile.DropDownItems.Add($menuExit)
    $menuMain = New-Object System.Windows.Forms.MenuStrip
    $mainToolStrip = New-Object System.Windows.Forms.ToolStrip
    # Main ToolStrip
    [void]$form1.Controls.Add($mainToolStrip)
 
    # Main Menu Bar
    [void]$form1.Controls.Add($menuMain)
    $form1.ShowDialog() | Out-Null
    #Invoke-item "$WorkingFolder\SPLITNAVOBJ\"
}
NAVObjectPackageConfigurator


Wegen des intensiven GUI-Einsatzes wird mindestens PowerShell Version 3 benötigt, aber das sollte mittlerweile ja kein Problem mehr darstellen :-) .

Links zum Einsatz des Treeviewcontrols in PowerShell
How Can I Use the Windows Forms TreeView Control? (die dort anfangs benutzte Sapien PrimalForms Community Editon, um den für Forms notwendigen Quellcode schnell zu generieren, gibt es leider nicht mehr)
Object Package Configurator Objektpaket Konfigurator
Du hast keine ausreichende Berechtigung, um die Dateianhänge dieses Beitrags anzusehen.