TechnikBlog

Jul 04
Teradata / PDW Problem unter Reporting Services und SharePoint 2013

Ich bin heute über ein Problem mit SharePoint 2013 und den Reporting Services des SQL Servers 2014 im SharePoint integrierten Modus gestolpert. Im Event-Log wird folgendes geloggt.

Und zwar für Teradata und SQLPDW. Die Erklärung ist ziemlich einfach. Die Komponenten sind zwar in Reporting Services standardmäßig registriert, werden aber bei der Installation der RS nicht mit installiert. Abhilfe schafft das kleine PowerShell Script was die entsprechenden Komponenten aus der Konfiguration entfernt.

$ssrs = Get-SPRSServiceApplication    
$ext = Get-SPRSExtension -Identity $ssrs.id

# FGE: TERADATA entfernen   
$e = $ext |   
   Where-Object {$_.Name -eq "TERADATA" -and $_.ExtensionType -eq "Data"}
Remove-SPRSExtension `
   -Name $e.Name `
  
-ExtensionType $e.ExtensionType `
  
-Identity $ssrs.Id
$e = $ext |   
  
Where-Object {$_.Name -eq "TERADATA" -and $_.ExtensionType -eq "SemanticQuery"}
Remove-SPRSExtension `
  
-Name $e.Name `
  
-ExtensionType $e.ExtensionType `
  
-Identity $ssrs.Id
$e = $ext |   
  
Where-Object {$_.Name -eq "TERADATA" -and $_.ExtensionType -eq "ModelGeneration"}
Remove-SPRSExtension `
  
-Name $e.Name `
  
-ExtensionType $e.ExtensionType `
  
-Identity $ssrs.Id

# FGE: PDW entfernen   
$e = $ext |   
  
Where-Object {$_.Name -eq "SQLPDW" -and $_.ExtensionType -eq "Data"} Remove-SPRSExtension `
  
-Name $e.Name `
  
-ExtensionType $e.ExtensionType `
  
-Identity $ssrs.Id
$e = $ext |   
Where-Object {$_.Name -eq "SQLPDW" -and $_.ExtensionType -eq "SemanticQuery"}
Remove-SPRSExtension `
  
-Name $e.Name `
  
-ExtensionType $e.ExtensionType `
  
-Identity $ssrs.Id

Jun 04
Modulare Entwicklung in PowerShell

PowerShell ist ein sehr mächtiges Werkzeug geworden mit dem man sehr schnell komplexe Scripte schreiben kann die beispielsweise komplette Infrastrukturen in Azure aufbauen können.

Funktionen

Wie in jeder anderen Programmiersprache auch neigen komplexe Programmierungen schnell dazu, unübersichtlich zu werden, da ist auch PowerShell keine Ausnahme. Zum Glück kann man Funktionalität in PowerShell in Funktionen kapseln. Eine Funktion ist dabei recht einfach aufgebaut. Sie beginnt mit dem Schlüsselwort Function, ggf. einer Parameterliste in Klammern und dann einem Block der von geschweiften Klammern umgeben ist in dem die eigentliche Funktionalität der Funktion untergebracht ist. Als Beispiel habe ich einmal die folgende Funktion herausgesucht die den Namen der aktuelle Administratorengruppe ermittelt:

Function Get-AdministratorsGroup    
{
   if(!$builtinAdminGroup)
   {
     $builtinAdminGroup = (Get-WmiObject -Class Win32_Group `
         -computername $env:COMPUTERNAME `
         -Filter "SID='S-1-5-32-544' `
                 AND LocalAccount='True'" `
         -errorAction "Stop").Name
   }
   Return $builtinAdminGroup    
}

Was die Funktion im Einzelnen tut soll uns an dieser Stelle einmal egal sein, es geht uns nur darum, dass wir eine Funktion haben die irgendeine Funktionalität kapselt. Die Funktion kann man nun in PowerShell mit dem Befehl

Get-AdministratorsGroup

aufrufen. Wie erwartet wird der Name der lokalen Administratoregruppe zurückgeliefert:

PS C:\Windows\system32> Get-AdministratorsGroup
Administratoren

Das Aufteilen von Funktionalität innerhalb eines PowerShell Scriptes mit Hilfe von Funktionen ist schon mal ein erster Schritt zur Modularisierung, führt aber dazu, dass das Script immer noch sehr lang ist, was der Übersichlichkeit nicht zugutekommt. Außerdem wollen wir unsere Funktion ja vielleicht auch in anderen Scripten weiterverwenden und Copy & Paste ist aus meiner Sicht hier der falsche Ansatz.

Include

Zum Glück bietet PowerShell aber auch hier eine wertvolle Hilfe an. Man kann Scripte, so wie man es von anderen Programmiersprachen her kennt in andere Scripte "includen", d.h. am Anfang eines Scriptes kann man ein anderes Script laden. Auch das möchte ich hier kurz demonstrieren. Dazu speichere ich die Funktion Get-AdministratorsGroup in einer eigenen Datei ab die ich Adminfunctions.ps1 nenne. Nun schreibe ich ein zweites Script das ich main.ps1 nenne. Das Ergebnis sieht dann so aus:

Nun kann ich das Script AdminFunctions.ps1 in das Script Main.ps1 einbinden, indem ich folgendes programmiere:

. "$PSScriptRoot\Adminfunctions.ps1"      

Get-AdministratorsGroup

Ich kann ein Script also in ein anderes Script über den Punkt-Operator (.) einbinden. Schon mal gut zu wissen. Wichtig bei dieser Aktion ist, dass man den Pfad an dem das einzubindende Script liegt exakt angibt, da das einzubindende Script sonst vom einbindenden Script aus logischerweise nicht gefunden werden kann. Befindet sich das einzubindende Script im gleichen Verzeichnis wie das einbindende Script kann man das so wie oben im Code zu sehen machen, indem man sich über die Variable $PSScriptRoot das aktuelle Verzeichnis besorgt in dem das einbindende Script liegt. Ist das nicht der Fall muss man über die Möglichkeiten die PowerShell zur Navigation im Dateisystem bietet an die richtige Stelle verzweigen. Lassen wir unser Script laufen bekommen wir das folgende Ergebnis:

PS C:\Windows\system32> Get-AdministratorsGroup
Administratoren

PowerShell Module

Neben der Einbindung von Scripten über den Punkt-Operator können ist es auch möglich in PowerShell so genannte PowerShell Module zu schreiben. Im Prinzip ist ein PowerShell-Modul nichts anderes als eine "normale" PowerShell Script-Datei mit dem Dateisuffix .psm1. Diese Datei muss man in einen Ordner legen der denselben Namen besitzt wie die Datei selbst. Idealerweise legt man diesen Ordner in einem Ordner Module ab, wo sich sämtliche PowerShell Module befinden die man so benötigt. Auch dies möchte ich an unserem einfachen Beispiel einmal demonstrieren.

Hierzu nenne ich die Datei AdminFunctions.ps1 in Adminfunctions.psm1 um, erstelle einen Ordner Modules\AdminFunctions und verschiebe die Datei in diesen Ordner.

Wir sind an dieser Stelle schon fast fertig, eine wichtige Voraussetzung dass wir unser PowerShell Modul mit Import-Module laden können fehlt aber noch. Wir müssen den Pfad den wir für die Module erstellt haben (also Power Shell Scripts\Test\Modules) noch in die Umgebungsvariable $env:PSModulePath einbinden. In dieser Variablen sind alle Orte gespeichert in denen PowerShell beim Aufruf von Import-Module nachschaut. Hat man ein Script das selbst ein Modulverzeichnis mitbringt ist es am einfachsten dieses Modulverzeichnis über den folgenden Befehl der Environment-Variablen anzuhängen:

if (!($env:PSModulePath -like "*$PSScriptRoot*"))
{
  $env:PSModulePath = $env:PSModulePath+";"+$PSScriptRoot+"\Modules"          
}

Der Code ist sehr simpel aufgebaut. Zunächst wird geschaut ob der aktuelle Sciptroot ($PSScriptRoot) bereits im Modulpfad ($env:PSModulePath) enthalten ist. Ist das nicht der Fall, dann wird der neue Modulpfad einfach durch Semikolon getrennt an den aktuellen Modulpfad angehängt. Die If-Abfrage ist dahingehend wichtig als dass ansonsten der Modulpfad bei jedem Aufruf des Scriptes an die Umgebungsvariable angehängt würde, was wir ja so nicht wollen. Nachdem unser Modulpfad nun bekannt ist können wir unser eigenes PowerShell Modul mit Import-Module laden. Der Code des aufrufenden Scriptes sieht also wie folgt aus:

if (!($env:PSModulePath -like "*$PSScriptRoot*"))
{
   $env:PSModulePath = $env:PSModulePath+";"+$PSScriptRoot+"\Modules"        
}

Import-Module
-name "AdminFunctions"    

Get-AdministratorsGroup

Dass unser Modul geladen wurde kann man einerseits daran sehen, dass Get-AdministratorsGroup funktioniert und andererseits kann man es auch mit Get-Module überprüfen.

PS C:\Windows\system32> Get-Module

ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Script 0.0 AdminFunctions Get-AdministratorsGroup
Script 1.0.0.0 ISE {Get-IseSnippet, Import-IseSnippet, New-IseSnippet}
Manifest 3.1.0.0 Microsoft.PowerShell.Management {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Content...}
Manifest 3.0.0.0 Microsoft.PowerShell.Security {ConvertFrom-SecureString, ConvertTo-SecureString, Get-Acl, Get-AuthenticodeSignature...}
Manifest 3.1.0.0 Microsoft.PowerShell.Utility {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Manifest 3.0.0.0 Microsoft.WSMan.Management {Connect-WSMan, Disable-WSManCredSSP, Disconnect-WSMan, Enable-WSManCredSSP...}

Das war es eigentlich auch schon. Ist meiner Meinung nach relativ simpel und "straight forward". Viel Spaß beim Aufteilen Ihrer PowerShell Scripte in Includes und Module.

Apr 08
Mit Vorlagen in T-SQL arbeiten
 

​In diesem Video demonstriert Frank Geisler wie man sowohl unter dem SQL Management Studio wie auch unter den SQL Server Data Tools mit Vorlagen und Snippets arbeiten kann und sich so beim Erstellen von T-SQL Code viel Arbeit spart. Das Video unterstützt den SQL Podcast 11 - T-SQL on Steroids, der von Tillmann Eitelberg zusammen mit Frank Geisler aufgenommen wurde.

Mrz 29
Daten mit Power Query importieren und bearbeiten
 

In diesem rund 9 Minuten langen Video zeigt Frank Geisler wie man mit Hilfe von Power Query Daten von einer Website im Internet importieren kann und diese dann über die zahlreichen Funktionen die Power Query anbietet so umwandeln kann, dass sie für eine Business Intelligence Auswertung verwendet werden können.

Feb 24
Beliebige Tabelleninhalte in Reporting Services addieren

Heute hatte ich einen Fall bei dem ich innerhalb einer Reporting Services Tablix eine Summe bilden sollte. Das alleine ist nicht besonders herausfordernd. Schwieriger wird es allerdings, wenn es sich bei den zu summierenden Werten um Werte handelt die innerhalb der Tablix selbst durch eine nicht-additive Aggregatsfunktion entstehen und somit auch nicht die Summe über das Dataset gezogen werden kann. Bei mir entstanden die Werte innerhalb der Tablix über die Funktion FIRST. Würde ich in diesem Fall die Summe über das Dataset ziehen hätte ich einen viel zu hohen Wert.

Die Lösung dieser Aufgabe liegt in VBA-Code den man hinter den Bericht legt. Die Idee: Man deklariert eine Variable und addiert in einer VBA-Funktion alle Werte auf diese Variable. Dann kann man diesen Wert später in der Summenzeile anzeigen.

Der VBA-Code sieht wie folgt aus. Zunächst die Variablendeklaration:

Dann die Funktion

Im Bericht schreibt man dann in das Feld in dem die zu addierenden Werte angezeigt werden den folgenden Ausdruck:

Und dort wo die Summe erscheinen soll kommt dieser Ausdruck hin:

Und schon werden die Zahlen die über das Dataset nicht additiv sind summiert.

Jul 25
Kein Syntax-Highlighting nach Installation der neuen Version der SQL Server Datatools

Ich habe mir heute morgen mal die neue Version der SQL Server Datatools heruntergeladen und installiert. Hat auch erstmal alles soweit geklappt. Bis auf, dass nach der Installation leider das Syntax-Highlighting für SQL nicht mehr funktioniert hat:

Das ist natürlich besonders doof und so kann man nicht vernünftig arbeiten. Zum Glück gibt es einen einfachen Fix. Wenn man das Problem hat muss man einfach nur eine Visual Studio Kommandozeile als Administrator öffnen und dort DEVENV /Setup eingeben.

Nach einer kurzen Wartezeit kann man dann Visual Studio wieder starten und das Syntax-Highlighting ist wieder da.

Jul 17
SQL Server Backup in der Cloud

Eine der interessanten neuen Funktionen, die der SQL-Server 2014 bietet, ist die Möglichkeit Datenbank-Backups direkt in die Cloud durchzuführen. So ist es auch kleinen Unternehmen mit nur einem Standort möglich automatisch Backups anzufertigen die nicht am Standort selbst verbleiben sondern außerhalb gelagert werden. Sollte am Standort dann einmal eine Katastrophe passieren (z.B. dass der Keller in dem der Datenbankserver steht überflutet wird), so sind die Daten nicht weg sondern immer noch verfügbar. Wie genau man den SQL Server so einrichtet, dass das Datenbank-Backup in der Cloud abgelegt wird werde ich in diesem Blog-Artikel beschreiben. Da die Daten in der Cloud abgelegt werden ist es natürlich wichtig, dass das Datenbankbackup verschlüsselt wird. Wie das funktioniert werde ich auch in diesem Artikel erklären.

Die erste Voraussetzung die man schaffen muss um ein SQL Server Backup in der Cloud abzulegen ist, dass man dort natürlich einen Speicherplatz zur Verfügung stellen muss in dem das Backup gespeichert wird. Unter Azure heißt ein solcher Speicherplatz BLOB Storage. Melden Sie sich am Windows Azure Portal an, wählen Sie links in der Spalte Speicher aus. Klicken Sie dann unten auf das Icon Neu.

Wählen Sie im Dialog die Schnellerfassung aus, geben Sie eine URL ein, wählen Sie einen Standort aus und ob Ihre Backups georedundant gespeichert werden sollen oder nicht. Geben Sie an, dass Ihre Backups georedundant gespeichert werden sollen haben Sie noch mehr Sicherheit, da Ihre Daten selbst dann noch vorhanden sind wenn es im Microsoft Rechenzentrum, das Sie zur Speicherung Ihrer Daten angegeben haben, zu einem Problem kommt.

Nachdem der BLOB-Storage angelegt wurde müssen Sie noch einen Container definieren. Klicken Sie dazu den neuen Speicher an und wählen Sie im Menü oben Container aus und klicken Sie dann auf den Link Container erstellen.

Geben Sie nun einen Namen für den neuen Container ein.

Den letzten Schritt den Sie nun noch mit dem BLOB-Storage unternehmen müssen ist, dass Sie den Schlüssel für den Zugriff auf den Storage exportieren müssen. Klicken Sie hierzu auf den Link Dashboard und dann unten auf das Icon Zugriffschlüssel verwalten.

Im Dialog "Zugriffsschlüssel verwalten" kopieren Sie den Primären Zugriffschlüssel ins Clipboard, wir benötigen ihn nachher noch.

So, auf der Seite von Azure ist nun alles eingerichtet. Als nächstes müssen Sie die Azure Zugriffsinformationen im SQL Server ablegen. Geben Sie hierzu den folgenden SQL Befehl ein (natürlich mit Ihrem Zugriffschlüssel an der richtigen Stelle):

CREATE CREDENTIAL GDSDBBackup
WITH IDENTITY = 'gdstest',
SECRET = '>>> Hier kommt der Zugriffschlüssel hin <<<'

GO


Erstellt man ein Backup in der Cloud ist es ratsam dieses Backup auch zu verschlüsseln. Eine weitere neue Funktion des SQL Servers 2014 ist die Backup-Verschlüsselung. Bevor man diese aber nutzen kann muss man sie zuerst einrichten. Der erste Schritt ist es, den Service Master Key auf Festplatte zu speichern. Das geht mit dem folgenden Befehl:

-- FGE: Service Master Key speichern

BACKUP SERVICE MASTER KEY

TO FILE = 'C:\Temp\Service_master_key.key'

ENCRYPTION BY PASSWORD = '!test123';

GO

 

Hat man den Service Master Key gespeichert muss man als Nächstes einen neuen Datenbank Master Key erstellen. Das funktioniert mit dem folgenden Befehl:

 

-- FGE: Neuen Datenbank Master Key erstellen

CREATE MASTER KEY ENCRYPTION BY PASSWORD = '!test123';

GO

 

Natürlich sollte man auch diesen Schlüssel auf Festplatte speichern. Das stellt man mit dem folgenden Befehl an.

 

-- FGE: Neuen Master Key sichern

BACKUP MASTER KEY TO FILE = 'C:\Temp\DB_master_key.key'

ENCRYPTION BY PASSWORD = '!test123';

GO

 

Nun muss ein Zertifikat erstellt werden mit dem dann das Datenbank-Backup verschlüsselt werden kann. Dazu dient der folgende Befehl:

 

-- FGE: Zertifikat erzeugen

CREATE CERTIFICATE GDSMainDB_BackupEncryption

WITH SUBJECT = 'Zertifikat für die SQL Server Backup Verschlüsselung';

GO

 

Dieses Zertifikat sollte man natürlich auch noch exportieren, da man diesen Schlüssel zum Entschlüsseln des Datenbankbackups benötigt. Neben dem öffentlichen Schlüssel muss man auch den privaten Schlüssel exportieren.

 

-- FGE: Zertifikat sichern

BACKUP CERTIFICATE GDSMainDB_BackupEncryption

TO FILE = 'C:\Temp\BackupEncryption.cer'

WITH PRIVATE KEY

(

FILE = 'C:\Temp\BackupEncryption_private_key.key'

, ENCRYPTION BY PASSWORD = '!test123'

);

GO

 

Hat man alles richtig gemacht befinden sich jetzt auf der Festplatte im angegebenen Verzeichnis vier Dateien.

Nun kann man z.B. mit Hilfe des Backup-Assistenten ein verschlüsseltes Backup in Windows Azure anlegen. Wählen Sie unter Destination Back up to: URL aus und geben Sie einen Dateinamen ein. Im Auswahlfeld "SQL credential" können Sie die oben angelegten Anmeldeinformationen auswählen. Unter "Azure Storage Container" wird der oben angelegten Container eingegeben.

Um das Datenbank-Backup zu verschlüsseln müssen Sie auf der Seite "Backup Options" unten die Verschlüsselung mit dem oben angelegten Verschlüsselungsschlüssel auswählen:

Wenn Sie das Backup nun starten wird wie üblich ein Backup angelegt nur dieses Mal wird das Backup in der Cloud gespeichert. Natürlich kann man das alles auch scripten:

BACKUP DATABASE [ReportServer]

TO URL = N'https://>>>AzureURL<<</dbbackup/ReportServer_backup_2014_07_16_222744.bak'

WITH CREDENTIAL = N'GDSDBBackup',

NOFORMAT,

NOINIT,

NAME = N'ReportServer-Full Database Backup',

NOSKIP,

NOREWIND,

NOUNLOAD,

ENCRYPTION(ALGORITHM = AES_256, SERVER CERTIFICATE = [GDSMainDB_BackupEncryption]),

STATS = 10

GO

 

Schaut man in den Azure BLOB Storage, so findet man das Backup dort.

Mit Hilfe des Scripts kann man nun einen Backup-Job schreiben der die Datenbank regelmäßig in Azure speichert.

Jun 26
Zwei Datenbanken automatisch miteinander vergleichen

Wenn man in der Datenbankentwicklung tätig ist kommt es des Öfteren mal vor, dass man zwei Datenbanken miteinander vergleichen möchte wie beispielsweise das Produktivsystem mit dem Testsystem oder zwei Testsysteme untereinander. Besonders cool wäre eine Lösung mit der man Datenbanken automatisch zeitgesteuert vergleichen kann um Unterschiede aufzudecken und aus dem Ergebnis dieser Aktion dann einen Bericht zu generieren der aufzeigt was in den Datenbanken unterschiedlich ist. Natürlich geht das wunderbar über die Oberfläche der SQL Server Data Tools:

Der Nachteil an dieser Methode liegt aber darin, dass man das nicht wirklich automatisieren kann. Zum Glück ist aber auch das Kommandozeilen-Tool sqlpackage.exe Bestandteil der SQL Server Data Tools mit dem man einen Vergleich automatisieren kann. Ich habe keinen Weg gefunden zwei Datenbanken direkt zu vergleichen, daher gehe ich den Weg zunächst aus beiden Datenbanken DACPACs zu exportieren und diese dann miteinander zu vergleichen. Dazu habe ich das folgende PowerShell Script geschrieben (geht aber auch als normale Batch-Datei).

Cls

cd "C:\Program Files (x86)\Microsoft SQL Server\120\DAC\bin"

#FGE: Extract local Database

.\sqlpackage /Action:Extract /SourceConnectionString:"Data Source=192.168.137.119;Initial Catalog=TestDB;Persist Security Info=True;User ID=Admin;Password=!test123" /OverwriteFiles:True /TargetFile:"C:\temp\LocalDatabase.dacpac"

cd "C:\Program Files (x86)\Microsoft SQL Server\120\DAC\bin"

#FGE: Extract production Database

.\sqlpackage /Action:Extract /SourceConnectionString:"Data Source=192.168.137.119;Initial Catalog=TestDB2;Persist Security Info=True;User ID=Admin;Password=!test123" /OverwriteFiles:True /TargetFile:"C:\temp\ProductionDatabase.dacpac"

#FGE: Generate DeployReport

.\sqlpackage /Action:DeployReport /SourceFile:"C:\temp\LocalDatabase.dacpac" /TargetFile:"C:\temp\ProductionDatabase.dacpac" /OverwriteFiles:True /OutputPath:"C:\temp\DeployReport.xml" /TargetDatabaseName:"TestDB2"

   

Die erste Zeile extrahiert die lokale Datenbank, die zweite Zeile die Produktionsdatenbank als DACPAC. In der dritten Zeile wird von der lokalen Datenbank auf die Produktionsdatenbank verglichen, d.h. der generiert Deployment Bericht zeigt an was an der Produktivdatenbank geändert werden muss, damit sie auf dem gleichen Stand wie die lokale Datenbank ist. In unserem Beispiel habe ich in der lokalen Datenbank eine Stored Procedure mit vier Extended Properties angelegt. Der generierte Deployment Bericht sieht dementsprechend aus:

<?xml version="1.0" encoding="utf-8"?>                 
<DeploymentReport xmlns="http://schemas.microsoft.com/sqlserver/dac/DeployReport/2012/02">           
<Alerts />     
  <Operations>     
      <Operation Name="Create">           
         <Item Value="[dbo].[testproc]" Type="SqlProcedure" />                  
         <Item Value="[dbo].[testproc].[Execution_Sample]" Type="SqlExtendedProperty" />                  
         <Item Value="[dbo].[testproc].[MS_Description]" Type="SqlExtendedProperty" />                  
         <Item Value="[dbo].[testproc].[Author]" Type="SqlExtendedProperty" />                 
         <Item Value="[dbo].[testproc].[Project]" Type="SqlExtendedProperty" />                 
      </Operation>     
  </Operations>     
</DeploymentReport>
 

Jun 25
Erstes Video zu AlwaysOn
 

​In diesem Video zeigen Klaus Höltgen und Frank Geisler wie man eine Domäne für eine SQL Server 2014 AlwaysOn Installation einrichtet.​


Jun 25
URIs in einen Treeview laden

Heute hat mich ein Kollege angesprochen und gefragt wie man ein Array aus URIs in einen Treeview laden kann, d.h. die hierarchische Struktur die in einer URI vorherrscht und durch die Slashes (/) symbolisiert wird soll in eine echte hierarchische Struktur im Treeview aufgelöst werden. Also aus dem hier:

_myURIs[0] = new Uri(http://www.pentagon.org/home);
_myURIs[1] = new Uri(http://www.pentagon.org/secretStuff1);
_myURIs[2] = new Uri(http://www.pentagon.org/secretStuff1/TopSecret);
_myURIs[3] = new Uri(http://www.pentagon.org/secretStuff2/Classified);
_myURIs[4] = new Uri(http://www.pentagon.org/secretStuff3);
_myURIs[5] = new Uri("http://www.pentagon.org/secretStuff2/DontTouch/Really/IMeanIt/Seriously"); _myURIs[6] = new Uri("http://www.pentagon.org/");  

Soll das hier werden:

 

Wenn man das Ganze als rekursive Funktion angeht ist es eigentlich nicht so schwierig und recht elegant in ein paar Zeilen Code hingeschrieben. Zunächst brauchen wir eine übergeordnete Funktion die die Root-Knoten behandelt und dann die Rekursion einleitet. So eine Funktion kann wie folgt aussehen:

foreach (Uri _curUri in _myURIs)
{
 
if (treeView1.Nodes.Find(_curUri.Host, false).Length == 0)
   
treeView1.Nodes.Add(_curUri.Host, _curUri.Host);
  
// Wir wollen keine Slashes    
 
fillUriInTree(_curUri.Segments.Where(w => w != "/").ToArray(),
       treeView1.Nodes.Find(_curUri.Host, false)[0]);
}

Die If-Abfrage prüft, ob es den Host bereits als Root-Knoten gibt. Ist das nicht der Fall, so wird der Host als Root-Knoten angelegt. Das ist wichtig, damit der folgende Code diesen Knoten auch finden kann. Wenn es den Knoten schon gibt umso besser, dann muss nichts gemacht werden und der Knoten wird automatisch gefunden. In der Methode Nodes.Find geben wir als zweiten Parameter false an, da nicht in untergeordneten Knoten gesucht werden soll.

In der zweiten Zeile Code rufen wir unsere rekursive Füllfunktion auf die zunächst die über Uri.Segments in ein String-Array zerlegte URI (ohne Host) bekommen soll. Da Uri.Segments im Ergebnis-Array auch die Slashes (/) als eigene Elemente mitliefert (die wir aber im Tree nicht wollen) werden diese über eine LINQ-Abfrage rausgefiltert.

Kommen wir nun zur rekursiven Funktion. Die sieht wie folgt aus:

private void fillUriInTree (string[] pUriSegments, TreeNode pCurrentNode)
{
  if (pUriSegments.Length > 0)
 
{
   
// FGE: gibt es den Knoten auf der aktuellen Ebene?    
   
if (pCurrentNode.Nodes.Find(pUriSegments[0].Replace("/",""),
        false).Length != 0)
   
{
     
// FGE: Gibts schon -> entfernen und eine Ebene tiefer weitermachen
     
fillUriInTree(pUriSegments.Where(w => w != pUriSegments[0]).ToArray(), 
          pCurrentNode.Nodes.Find(pUriSegments[0].Replace("/", ""), false)
          [0]);
    
}
   
else    
   
{
    
// FGE: gibt es noch nicht -> Anlegen und eine Ebene tiefer weitermachen    
    
fillUriInTree(pUriSegments.Where(w => w != pUriSegments[0]).ToArray(), 
        pCurrentNode.Nodes.Add(pUriSegments[0].Replace("/", ""),
        pUriSegments[0].Replace("/", "")));
    }
 
}
}  

Die rekursive Funktion bekommt die beiden Parameter pUriSegments und pCurrentNode übergeben. In pUriSegments werden die verbleibenden, noch nicht in den Treeview eingefügten Werte übergeben und in pCurrentNode befindet sich der aktuelle Treeview Knoten. Bei der ersten Rekursion ist das der komplette URI (ohne Host) und der Host-Knoten. Das wichtigste bei einer Rekursion ist die Abbruch-Bedingung, die als aller erstes geprüft wird. Die Funktion wird nur dann aufgerufen wenn sich noch Elemente im Array pUriSegments befinden. Ist das Array leer sind wir mit der Arbeit durch und die Funktion kann beendet werden. War das Array nicht leer wird überprüft ob es das aktuelle URI-Segment bereits als Knoten im Treeview gibt. Ist das der Fall, so wird über den LINQ Ausdruck einfach das erste (also nullte) Element des URI-Segment-Arrays entfernt und der gefundene Knoten im Treeview wird als neuer Ausgangsknoten für den erneuten Aufruf der Funktion verwendet. Gab es das URI-Segment im Treeview bisher nicht wird es in der Übergabe für den nächsten Rekursionsdurchgang erzeugt und gleichzeitig aus dem URI-Segment-Array gelöscht. Da am Ende der URI-Segmente immer noch der Slash (/) ist, wird dieser über das Replace glöscht.

1 - 10Next