combit List & Label 30 - .NET Hilfe
Einführung in die Programmierung / Beispiele / Allgemein / Verwenden des Repository-Modes
Verwenden des Repository-Modes

Mit Hilfe eines Repositories können die meisten Dateizugriffe auf ein selbst definiertes, virtuelles Dateisystem umgeleitet werden. Dies erlaubt z.B. alle Berichte und zugehörigen Dateien in einer Datenbank zu halten. Die Umsetzung einer solchen Strategie wird in den folgenden Abschnitten beschrieben. Einen Überblick über die Verwendung und Funktionsweise des Repositories kann direkt im Namespace combit.Reporting.Repository eingesehen werden. Im Folgenden soll gezeigt werden, wie das Repository in List & Label verwendet werden kann. Die mitgelieferten ASP.NET Programmierbeispiele zeigen die Verwendung in der Praxis.

 

Tipp

Im Folgenden werden Code-Ausschnitte gezeigt, die aufgrund der Übersichtlichkeit auf ein Minimum reduziert wurden. Etwaige gezeigten Hilfsfunktionen, können aber in den mitgelieferten ASP.NET vollständig eingesehen werden, da die nachfolgende Beschreibung auf die Implementierung des IRepository-Interfaces anhand einer SQLite-Datenbank und dessen anschließende Verwendung in der Praxis im Detail in den ASP.NET Beispielen basiert.

 

 Schritt 1: Festlegung der Datenhaltung
Das Verwalten der Dateien für List Label wie die Projektdateien für den Designer, P-Dateien für Druckereinstellungen, Inhaltsverzeichnisse, Grafiken, Shapefiles etc. werden in der Regel dem Dateisystem überlassen. Somit arbeitet List & Label auf der einfachen Basis von Dateinamen und dessen Pfaden.

Sobald man sich nun jedoch für das Repository entscheidet, muss man sich mit dieser Frage auseinander setzen und einen geeigneten "Speicher" dafür verwenden bzw. ansteuern, da nun nicht mehr mit Dateinamen gearbeitet wird sondern nur noch mit sogenannten Repository-IDs. Über diese IDs können dann die gefragten Elemente aus dem Repository abgefragt und verwendet werden. Zahlreiche Applikationen nutzen für die Datenhaltung in der Regel eine Art Datenbanksystem, so dass auch oft SQL Datenbanken zum Einsatz kommen. Aus diesem Grund wird hier nun für einfache Demonstrationszwecke eine SQLite-Datenbank verwendet. Hierfür wird der Namespace System.Data.SQLite aus dem .NET Framework genutzt:

public class SQLiteFileRepository
{
    private readonly IDbConnection _db;
    public SQLiteFileRepository(string databasePath)
    {
        bool needsDatabaseInit = !File.Exists(databasePath);
        _db = new SQLiteConnection("Data Source=" + databasePath);
        _db.Open();
        if (needsDatabaseInit)
            DropAndCreateTables();
    }
   
    private void DropAndCreateTables()
    {
        _db.CreateCommand(@"
            DROP TABLE IF EXISTS RepoItems;
            CREATE TABLE IF NOT EXISTS RepoItems (
                ItemID               TEXT,
                Type                 TEXT,
                Descriptor           TEXT,
                TimestampUTC         INT,
                FileContent          BLOB
            );").ExecuteNonQuery();
    }
}
 Schritt 2: Notwendige Implementierung des Interfaces IRepository

Nun muss die Implementierung des Interfaces IRepository durchgeführt werden, so dass man individuell auf die Anfragen von List & Label reagieren kann:

public class SQLiteFileRepository : IRepository
{
    public bool ContainsItem(string itemID)
    {
        // ...
    }
    public void CreateOrUpdateItem(RepositoryItem item, string userImportData, Stream sourceStream)
    {
        // ...
    }
    public void DeleteItem(string itemID)
    {
        // ...
    }
    public IEnumerable<RepositoryItem> GetAllItems()
    {
        // ...
    }
    public RepositoryItem GetItem(string itemID)
    {
        // ...
    }
    public void LoadItem(string itemID, Stream destinationStream, CancellationToken cancelToken)
    {
        // ...
    }
    public bool LockItem(string id)
    {
        // ...
    }
    public void UnlockItem(string id)
    {
        // ...
    }
}
 IRepository.ContainsItem

Gemäß der Beschreibung in IRepository.ContainsItem wird diese Funktion aufgerufen, um zu prüfen ob ein bestimmtes Element im Repository existiert. Daher muss nun in der Datenbank angefragt werden, ob die angegebene ID vorhanden ist:

public class SQLiteFileRepository : IRepository
{
    // ...
   
    public bool ContainsItem(string itemID)
    {
        int result = Convert.ToInt32(_db.CreateCommand(
            "SELECT COUNT(*) FROM RepoItems WHERE ItemID = @ItemID")
               .SetParameter("ItemID", itemID).ExecuteScalar());
        return (result == 1);
    }
   
    // ...

}
 IRepository.CreateOrUpdateItem

Gemäß der Beschreibung in IRepository.CreateOrUpdateItem wird diese Funktion aufgerufen, wenn ein neues Element dem Repository hinzugefügt werden soll oder wenn ein vorhandenes Element aktualisiert werden soll. Jedoch wird diese Funktion auch aufgerufen, wenn sich lediglich die Metdaten des Elements aktualisieren oder diese unabhängig vom Inhalt hinzugefügt werden sollen:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void CreateOrUpdateItem(RepositoryItem item, string userImportData, Stream sourceStream)
    {
        // Den Stream von List & Label in ein byte array konvertieren, um es in die DB zu schreiben.
        // Hinweis: sourceStream kann null sein! Dann sollten nur die Metadaten in der Datenbank aktualisiert werden.
        byte[] fileContent = null;
        bool setMetadataOnly;
        if (sourceStream != null)
        {
            using (var memStream = new MemoryStream())
            {
                sourceStream.CopyTo(memStream);
                fileContent = memStream.ToArray();
            }
            setMetadataOnly = false;
        }
        else
        {
            setMetadataOnly = true;
        }

        // Muss ein neues Element aktualisiert werden on handelt es sich um ein neues Element?
        RepositoryItem itemToInsert;
        bool isUpdate = ContainsItem(item.InternalID);
        if (isUpdate)   
        {
            // Ein existierendes Repository-Item aktualisieren
            itemToInsert = GetItemsFromDb(item.InternalID).First();
            itemToInsert.Descriptor = item.Descriptor;
            itemToInsert.LastModificationUTC = item.LastModificationUTC;
        }
        else  
        {
            // Ein neues Repository-Item hinzufügen
            itemToInsert = new RepositoryItem(item.InternalID, item.Descriptor, item.Type, item.LastModificationUTC);
        }
       
        // Erstelle eine passende SQL-Abfrage für INSERT / UPDATE und rufe es mit oder ohne den Dateiinhalt auf.
        string sqlQuery;
        if (isUpdate)  // UPDATE
        {
            if (setMetadataOnly)
            {
                sqlQuery = @"UPDATE RepoItems
                             SET Descriptor = @Descriptor, TimestampUTC = @TimestampUTC
                             WHERE ItemID = @ItemID";
            }
            else
            {
                sqlQuery = @"UPDATE RepoItems
                             SET Descriptor = @Descriptor, TimestampUTC = @TimestampUTC, FileContent = @FileContent
                             WHERE ItemID = @ItemID";
            }
        }
        else    // INSERT
        {
            if (setMetadataOnly)
            {
                sqlQuery = @"INSERT INTO RepoItems (ItemID,  Type,  Descriptor,  TimestampUTC)
                                          VALUES  (@ItemID, @Type, @Descriptor, @TimestampUTC)";
            }
            else
            {
                sqlQuery = @"INSERT INTO RepoItems (ItemID,  Type,  Descriptor,  TimestampUTC,  FileContent)
                                          VALUES  (@ItemID, @Type, @Descriptor, @TimestampUTC, @FileContent)";
            }
        }
        _db.CreateCommand(sqlQuery)
            .SetParameter("ItemID", itemToInsert.InternalID)
            .SetParameter("Type", itemToInsert.Type)
            .SetParameter("Descriptor", itemToInsert.Descriptor)
            .SetParameter("FileContent", fileContent)
            .SetParameter("TimestampUTC", itemToInsert.LastModificationUTC.ToBinary())  // Es ist immer eine UTC-Zeit (muss für UI in eine lokale Zeit konvertiert werden)
            .ExecuteNonQuery();
    }
    // ...
}
 IRepository.DeleteItem

Gemäß der Beschreibung in IRepository.DeleteItem wird diese Funktion aufgerufen, wenn ein Element aus dem Repository entfernt werden soll. Daher muss nun auch der zugehörige Datesnsatz aus der SQLite-Datenbank entfernt werden:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void DeleteItem(string itemID)
    {
        _db.CreateCommand("DELETE FROM RepoItems WHERE ItemID = @ItemID")
            .SetParameter("ItemID", itemID)
            .ExecuteNonQuery();
    }
    // ...
}
 IRepository.GetAllItems

Diese Implementierung  wird aufgerufen, um alle vorhandenen Elemente in Repository abzufragen (siehe auch IRepository.GetAllItems):

public class SQLiteFileRepository : IRepository
{
    // ...
    public IEnumerable<RepositoryItem> GetAllItems()
    {
        List<RepositoryItem> result = new List<RepositoryItem>();
        var cmd = _db.CreateCommand("SELECT ItemID, Type, Descriptor, TimestampUTC, LENGTH(FileContent) FROM RepoItems");
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                result.Add(new RepositoryItem(
                    /* ItemID */ reader.GetString(0),
                    /* Descriptor */ reader.GetString(2),
                    /* Type */ reader.GetString(1),
                    /* TimestampUTC */ DateTime.FromBinary(reader.GetInt64(3)))
                {
                    IsEmpty = reader.IsDBNull(4) ? true : (reader.GetInt32(4) == 0 ? true : false)
                });
            }
        }
        return result;
    }
    // ...
}
 IRepository.GetItem

Um ein einzelnes Element aus dem Repository zurückliefern zu können, wird die Implementierung von IRepository.GetItem mit der angeforderten ID aufgerufen:

public class SQLiteFileRepository : IRepository
{
    // ...
    public RepositoryItem GetItem()
    {
        List<RepositoryItem> result = new List<RepositoryItem>();
        var cmd = _db.CreateCommand("SELECT ItemID, Type, Descriptor, TimestampUTC, LENGTH(FileContent) FROM RepoItems WHERE ItemID = @ItemId");
        cmd.SetParameter("ItemId", itemId);
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                result.Add(new RepositoryItem(
                    /* ItemID */ reader.GetString(0),
                    /* Descriptor */ reader.GetString(2),
                    /* Type */ reader.GetString(1),
                    /* TimestampUTC */ DateTime.FromBinary(reader.GetInt64(3)))
                {
                    IsEmpty = reader.IsDBNull(4) ? true : (reader.GetInt32(4) == 0 ? true : false)
                });
            }
        }
        return result.FirstOrDefault();
    }
    // ...
}
 IRepository.LoadItem

Wenn nun tatsächlich ein Element inhaltlich aus dem Repository geladen wird, um bspw. eine Projektdatei im Designer zu öffnen, wird die Implementierung von IRepository.LoadItem aufgerufen und es muss der Inhalt des angefragten Elements als Stream zurückgeliefert werden:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void LoadItem(string itemID, Stream destinationStream, CancellationToken cancelToken)
    {
        byte[] content = (byte[])_db.CreateCommand(
             "SELECT FileContent FROM RepoItems WHERE ItemID = @ItemID")
                .SetParameter("ItemID", itemID).ExecuteScalar();
        destinationStream.Write(content, 0, content.Length);
    }
    // ...
}
 IRepository.LockItem

Wenn die Anforderung besteht, dass ein Element nur exklusiv editiert werden kann - bspw. wenn eine Projektdatei im Designer geöffnet wird - so kann man einen exklusiven Zugriff mit Hilfe von IRepository.LockItem implementieren:

public class SQLiteFileRepository : IRepository
{
    // ...
    public bool LockItem(string id)
    {
        // WICHTIG: Es muss immer ein Fallback vorhanden sein, um etwaige Locks freizugeben (z.B. bei Netzwerk Timeouts).
        // Insbesondere in Netzwerk-Anwendungen kann unter Umsänden UnlockItem() nicht mehr aufgerufen werden wg. Netzwerk Problemen.

        // true zurückiefern, wenn der Lock angefordert wurde order wenn kein Lock implementiert ist.
        // false zurückliefern, wenn das Element von einem anderen Benutzer bereits gelockt wurde. Der Designer zeigt dann
        // einen Fehlermeldung an und öffnet das Element read-only mode.
        return true;
    }
    // ...
}
 IRepository.UnlockItem

Dies wird benötigt, um ein zuvor über IRepository.LockItem gesperrtes Element wieder freizugeben:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void UnlockItem(string id)
    {
        // ...
    }
    // ...
}
 Hilfsfunktion um Metadaten eines Repository Elements zu aktualisieren

Um einzelne Metadaten eines Elementes im Repository wie bspw. den Anzeigename im Designer einer Projektdatei modifizieren zu können, bedarf es einer kleinen Hilfsfunktion, die diese Anpassung in der SQLite-Datenbank überträgt:

public class SQLiteFileRepository : IRepository
{
    // ...
    public void SetItemMetadata(string itemID, string descriptor)
    {
        _db.CreateCommand(@"
             UPDATE RepoItems
             SET Descriptor = @Descriptor
             WHERE ItemID = @ItemID")
                .SetParameter("Descriptor", descriptor)
                .SetParameter("ItemID", itemID)
                .ExecuteNonQuery();
    }
    // ...
}

Ab nun werden alle List & Label betreffenden Dateien in einer SQLite-Datenbank verwaltet. Wie nun mit dem Repository gearbeitet werden kann wird unter "Schritt 3: Verwendung der eigenen IRepository Implementierung" beschrieben.

 Schritt 3: Verwendung der eigenen IRepository Implementierung

Hilfsklasse für den einfachen Zugriff auf das Repository

Mit dieser Klasse wird der Zugriff auf das selbst implementierte Repository vereinfacht und dient auch als Grundlage für die folgenden Punkte:

// Hilfsklasse
public class RepositoryHelper
{
    // Das aus Schritt 2 implementierte IRepository-Interface
    private static SQLiteFileRepository _fileRepository;
    public static SQLiteFileRepository GetCurrentRepository()
    {
        if (_fileRepository == null)
        {
            _fileRepository = new SQLiteFileRepository(Global.RepositoryDatabaseFile);
        }
       
        return _fileRepository;
    }
   
    // Definiert einen Anzeigenamen für ein Repository-Item.
    // Dieser Anzeigename wird nur für die Dialoge des Designers verwendet!
    // Das Repository-Item wird weiterhin nur über seine ID referenziert.
    public static void SetRepositoryItemProperties(string itemId, string name)
    {
        RepostoryItem modifiedItem = GetCurrentRepository().GetItem(itemId);

        // Rufe den (kodierten) Descriptor dieses Repository-Items ab (dieser enthält Metadaten wie den Anzeigenamen).
        string descriptorString = modifiedItem.Descriptor;

        // Dekodiere den String und setzte den gewünschten Anzeigenamen für das UI.
        var descriptor = RepositoryItemDescriptor.LoadFromDescriptorString(descriptorString);
        descriptor.SetUIName(0, name);   // 0 = Standardsprache
        descriptorString = descriptor.SerializeToString();

        // Speichere den geänderten Descriptor wieder im Repository.
        GetCurrentRepository().SetItemMetadata(itemId, descriptorString);
    }
}

 

Hinzufügen/Importieren von bestehenden Dateien ins Repository

Damit vorhandene Dateien wie bspw. Projektdateien, Grafiken, Shapefiles etc. dem Repository hinzugefügt werden können, kann dies mit der Hilfsklasse RepositoryImportUtil durchgeführt werden:

// Führt eine passende Importfunktion für den Dateityp aus.
private void AddFileToRepository(RepositoryItemType fileType, string file1, string file2)
{
    string createdItemId1 = null;
    string createdItemId2 = null;
    // Die RepositoryImportUtil-Klasse hilft beim Anlegen neuer Einträge bzw. Importieren bestehender Dateien.
    using (RepositoryImportUtil util = new RepositoryImportUtil(RepositoryHelper.GetCurrentRepository()))
    {
        using (ListLabel LL = new ListLabel())
        {
            // Beachten Sie die Möglichkeit, eine eigene Information an die CreateOrUpdate()-Methode (in SQLiteFileRepository) zu übergeben,
            // die durch den Import intern aufgerufen wird. Diese Information ist dort im Parameter "userImportData" wieder verfügbar.
            string userImportData = "Some custom information for your repository";
            if (RepositoryItemType.IsProjectType(fileType))
            {
                createdItemId1 = util.ImportProjectFile(LL, file1, userImportData /* , printerConfigFile, sketchImageFile */);
            }
            else if (fileType == RepositoryItemType.Image)
            {
                createdItemId1 = util.ImportImageFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.PDF)
            {
                createdItemId1 = util.ImportPdfFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectReverseSide)
            {
                createdItemId1 = util.ImportReverseSideFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectTableOfContents)
            {
                createdItemId1 = util.ImportTableOfContentsFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.ProjectIndex)
            {
                createdItemId1 = util.ImportIndexFile(LL, file1, userImportData);
            }
            else if (fileType == RepositoryItemType.Shapefile)
            {
                // ImportShapeFile() liefert zwei IDs zurück
                // Für das Shapefile (*.shp) und die zugehörige Datenbankdatei (*.dbf)
                var createdShapeFileItems = util.ImportShapefile(LL, file1, file2, userImportData);
                createdItemId1 = createdShapeFileItems.Item1;  
                createdItemId2 = createdShapeFileItems.Item2;
            }
           
            // Lege den ursprünglichen Dateinamen (ohne Dateiendung) als Anzeigenamen des Repository-Items im UI fest.           
            string displayName1 = file1.FileName;
            if (fileType != RepositoryItemType.Shapefile)   // Behalte die Dateiendung nur bei Shapefiles, da es immer zwei Dateien mit dem gleichen Namen sind)
                displayName1 = Path.GetFileNameWithoutExtension(displayName1);
           
            RepositoryHelper.SetRepositoryItemProperties(createdItemId1, displayName1);
           
            if (createdItemId2 != null)
                RepositoryHelper.SetRepositoryItemProperties(createdItemId2, Path.GetFileNameWithoutExtension(file2));
        }
    }
}

 

Erstellen neuer Elemente (neuer Projektdateien) im Repository

Soll eine neue Projektdatei erstellt und im Designer bearbeitet werden, so bietet auch hier die Hilfsklasse RepositoryImportUtil die geeignete Funktion CreateNewProject an:

private void CreateNewRepositoryItem(LlProject projectType, string name)
{
    // Die RepositoryImportUtil-Klasse hilft beim Anlegen neuer Einträge bzw. Importieren bestehender Dateien.
    string createdItemID;
    using (RepositoryImportUtil util = new RepositoryImportUtil(RepositoryHelper.GetCurrentRepository()))
    {
        createdItemID = util.CreateNewProject(projectType, name);
    }

    // Wenn nicht nur die ID des Repository-Items angezeigt werden soll, muss ein Anzeigename für das UI festgelegt werden.
    RepositoryHelper.SetRepositoryItemProperties(createdItemID, name);
}

 

Entfernen von Elementen aus dem Repository

Um Elemente anhand seiner ID aus dem Repository zu entfernen, kann direkt die dafür vorgesehene Implementierung von IRepository.DeleteItem aufgerufen werden: 

private void DeleteRepositoryItem(String itemID)
{
    RepositoryHelper.GetCurrentRepository().DeleteItem(itemID);
}
Die mitgelieferten ASP.NET Programmierbeispiele zeigen die Implementierung des IRepository-Interfaces anhand einer SQLite-Datenbank und dessen anschließende Verwendung in der Praxis im Detail.