Datenbanken in Xamarin.Mac

In diesem Artikel wird die Verwendung von Key-Value-Codierung und Key-Value-Observing behandelt, um die Datenbindung zwischen SQLite-Datenbanken und UI-Elementen im Schnittstellen-Generator von Xcode zu ermöglichen. Außerdem wird die Verwendung des SQLite.NET ORM zum Bereitstellen des Zugriffs auf SQLite-Daten behandelt.

Übersicht

Beim Arbeiten mit C# und .NET in einer Xamarin.Mac-Anwendung haben Sie Zugriff auf dieselben SQLite-Datenbanken, auf die eine Xamarin.iOS- oder Xamarin.Android-Anwendung zugreifen kann.

In diesem Artikel werden wir zwei Möglichkeiten für den Zugriff auf SQLite-Daten behandeln:

  1. Direct Access – Durch den direkten Zugriff auf eine SQLite-Datenbank können wir Daten aus der Datenbank verwenden, um Schlüsselwertcodierung und Datenbindung mit UI-Elementen zu verwenden, die im Schnittstellen-Generator von Xcode erstellt wurden. Mithilfe von Schlüsselwertcodierungs- und Datenbindungstechniken in Ihrer Xamarin.Mac-Anwendung können Sie die Menge an Code, den Sie schreiben und Standard, um UI-Elemente aufzufüllen und zu bearbeiten, erheblich verringern. Außerdem profitieren Sie von der weiteren Entkoppelung Ihrer Sicherungsdaten (Datenmodell) von der Front-End-Benutzeroberfläche (Model-View-Controller), was zu einfacheren Standard nachhaltigen, flexibleren Anwendungsdesigns führt.
  2. SQLite.NET ORM – Mithilfe des Open Source-SQLite.NET Object Relationship Manager (ORM) können wir die Menge an Code erheblich reduzieren, der zum Lesen und Schreiben von Daten aus einer SQLite-Datenbank erforderlich ist. Diese Daten können dann verwendet werden, um ein Benutzeroberflächenelement wie eine Tabellenansicht aufzufüllen.

An example of the running app

In diesem Artikel behandeln wir die Grundlagen der Arbeit mit Key-Value-Codierung und Datenbindung mit SQLite-Datenbanken in einer Xamarin.Mac-Anwendung. Es wird dringend empfohlen, dass Sie zuerst den Artikel "Hello, Mac " durcharbeiten, insbesondere die Abschnitte "Einführung in Xcode" und "Interface Builder " und "Outlets" und "Actions ", da es sich um wichtige Konzepte und Techniken handelt, die wir in diesem Artikel verwenden werden.

Da wir schlüsselwertbasierte Codierung und Datenbindung verwenden werden, arbeiten Sie zuerst durch die Datenbindung und schlüsselwertbasierte Codierung , da kerne Techniken und Konzepte behandelt werden, die in dieser Dokumentation und ihrer Beispielanwendung verwendet werden.

Möglicherweise möchten Sie sich auch die Exposing C#-Klassen /-Methoden im Objective-C Abschnitt des Xamarin.Mac Internals-Dokuments ansehen. Außerdem werden die Register C#-Klassen und Export -Attribute erläutert, die zum Verbinden Ihrer C#-Klassen mit Objective-C Objekten und UI-Elementen verwendet werden.

Direkter SQLite-Zugriff

Für SQLite-Daten, die an UI-Elemente im Benutzeroberflächen-Generator von Xcode gebunden werden sollen, wird dringend empfohlen, direkt auf die SQLite-Datenbank zuzugreifen (im Gegensatz zur Verwendung einer Technik wie einem ORM), da Sie die Gesamtkontrolle darüber haben, wie die Daten geschrieben und aus der Datenbank gelesen werden.

Wie wir in der Dokumentation "Datenbindung" und "Key-Value Coding" gesehen haben, können Sie die Menge des Codes, den Sie Standard schreiben müssen, erheblich verringern, indem Sie schlüsselwertbasierte Codierungstechniken und Datenbindungstechniken in Ihrer Xamarin.Mac-Anwendung verwenden. In Kombination mit direktem Zugriff auf eine SQLite-Datenbank kann die Menge des Codes, der zum Lesen und Schreiben von Daten in diese Datenbank erforderlich ist, erheblich reduziert werden.

In diesem Artikel ändern wir die Beispiel-App aus dem Datenbindungs- und Schlüsselwertcodierungsdokument, um eine SQLite-Datenbank als Sicherungsquelle für die Bindung zu verwenden.

Einschließen der SQLite-Datenbankunterstützung

Bevor wir fortfahren können, müssen wir unserer Anwendung SQLite-Datenbankunterstützung hinzufügen, indem wir verweise auf ein paar von . DLLs-Dateien.

Gehen Sie folgendermaßen vor:

  1. Klicken Sie im Lösungspad mit der rechten Maustaste auf den Ordner "Verweise", und wählen Sie "Verweise bearbeiten" aus.

  2. Wählen Sie die Assemblys "Mono.Data.Sqlite " und "System.Data " aus:

    Adding the required references

  3. Klicken Sie auf die Schaltfläche "OK ", um Ihre Änderungen zu speichern und die Verweise hinzuzufügen.

Ändern des Datenmodells

Da wir nun Unterstützung für den direkten Zugriff auf eine SQLite-Datenbank zu unserer Anwendung hinzugefügt haben, müssen wir unser Datenmodellobjekt ändern, um Daten aus der Datenbank zu lesen und zu schreiben (sowie Schlüsselwertcodierung und Datenbindung bereitzustellen). Im Fall unserer Beispielanwendung bearbeiten wir die PersonModel.cs Klasse und machen sie wie folgt aussehen:

using System;
using System.Data;
using System.IO;
using Mono.Data.Sqlite;
using Foundation;
using AppKit;

namespace MacDatabase
{
    [Register("PersonModel")]
    public class PersonModel : NSObject
    {
        #region Private Variables
        private string _ID = "";
        private string _managerID = "";
        private string _name = "";
        private string _occupation = "";
        private bool _isManager = false;
        private NSMutableArray _people = new NSMutableArray();
        private SqliteConnection _conn = null;
        #endregion

        #region Computed Properties
        public SqliteConnection Conn {
            get { return _conn; }
            set { _conn = value; }
        }

        [Export("ID")]
        public string ID {
            get { return _ID; }
            set {
                WillChangeValue ("ID");
                _ID = value;
                DidChangeValue ("ID");
            }
        }

        [Export("ManagerID")]
        public string ManagerID {
            get { return _managerID; }
            set {
                WillChangeValue ("ManagerID");
                _managerID = value;
                DidChangeValue ("ManagerID");
            }
        }

        [Export("Name")]
        public string Name {
            get { return _name; }
            set {
                WillChangeValue ("Name");
                _name = value;
                DidChangeValue ("Name");

                // Save changes to database?
                if (_conn != null) Update (_conn);
            }
        }

        [Export("Occupation")]
        public string Occupation {
            get { return _occupation; }
            set {
                WillChangeValue ("Occupation");
                _occupation = value;
                DidChangeValue ("Occupation");

                // Save changes to database?
                if (_conn != null) Update (_conn);
            }
        }

        [Export("isManager")]
        public bool isManager {
            get { return _isManager; }
            set {
                WillChangeValue ("isManager");
                WillChangeValue ("Icon");
                _isManager = value;
                DidChangeValue ("isManager");
                DidChangeValue ("Icon");

                // Save changes to database?
                if (_conn != null) Update (_conn);
            }
        }

        [Export("isEmployee")]
        public bool isEmployee {
            get { return (NumberOfEmployees == 0); }
        }

        [Export("Icon")]
        public NSImage Icon {
            get {
                if (isManager) {
                    return NSImage.ImageNamed ("group.png");
                } else {
                    return NSImage.ImageNamed ("user.png");
                }
            }
        }

        [Export("personModelArray")]
        public NSArray People {
            get { return _people; }
        }

        [Export("NumberOfEmployees")]
        public nint NumberOfEmployees {
            get { return (nint)_people.Count; }
        }
        #endregion

        #region Constructors
        public PersonModel ()
        {
        }

        public PersonModel (string name, string occupation)
        {
            // Initialize
            this.Name = name;
            this.Occupation = occupation;
        }

        public PersonModel (string name, string occupation, bool manager)
        {
            // Initialize
            this.Name = name;
            this.Occupation = occupation;
            this.isManager = manager;
        }

        public PersonModel (string id, string name, string occupation)
        {
            // Initialize
            this.ID = id;
            this.Name = name;
            this.Occupation = occupation;
        }

        public PersonModel (SqliteConnection conn, string id)
        {
            // Load from database
            Load (conn, id);
        }
        #endregion

        #region Array Controller Methods
        [Export("addObject:")]
        public void AddPerson(PersonModel person) {
            WillChangeValue ("personModelArray");
            isManager = true;
            _people.Add (person);
            DidChangeValue ("personModelArray");
        }

        [Export("insertObject:inPersonModelArrayAtIndex:")]
        public void InsertPerson(PersonModel person, nint index) {
            WillChangeValue ("personModelArray");
            _people.Insert (person, index);
            DidChangeValue ("personModelArray");
        }

        [Export("removeObjectFromPersonModelArrayAtIndex:")]
        public void RemovePerson(nint index) {
            WillChangeValue ("personModelArray");
            _people.RemoveObject (index);
            DidChangeValue ("personModelArray");
        }

        [Export("setPersonModelArray:")]
        public void SetPeople(NSMutableArray array) {
            WillChangeValue ("personModelArray");
            _people = array;
            DidChangeValue ("personModelArray");
        }
        #endregion

        #region SQLite Routines
        public void Create(SqliteConnection conn) {

            // Clear last connection to prevent circular call to update
            _conn = null;

            // Create new record ID?
            if (ID == "") {
                ID = Guid.NewGuid ().ToString();
            }

            // Execute query
            conn.Open ();
            using (var command = conn.CreateCommand ()) {
                // Create new command
                command.CommandText = "INSERT INTO [People] (ID, Name, Occupation, isManager, ManagerID) VALUES (@COL1, @COL2, @COL3, @COL4, @COL5)";

                // Populate with data from the record
                command.Parameters.AddWithValue ("@COL1", ID);
                command.Parameters.AddWithValue ("@COL2", Name);
                command.Parameters.AddWithValue ("@COL3", Occupation);
                command.Parameters.AddWithValue ("@COL4", isManager);
                command.Parameters.AddWithValue ("@COL5", ManagerID);

                // Write to database
                command.ExecuteNonQuery ();
            }
            conn.Close ();

            // Save children to database as well
            for (nuint n = 0; n < People.Count; ++n) {
                // Grab person
                var Person = People.GetItem<PersonModel>(n);

                // Save manager ID and create the sub record
                Person.ManagerID = ID;
                Person.Create (conn);
            }

            // Save last connection
            _conn = conn;
        }

        public void Update(SqliteConnection conn) {

            // Clear last connection to prevent circular call to update
            _conn = null;

            // Execute query
            conn.Open ();
            using (var command = conn.CreateCommand ()) {
                // Create new command
                command.CommandText = "UPDATE [People] SET Name = @COL2, Occupation = @COL3, isManager = @COL4, ManagerID = @COL5 WHERE ID = @COL1";

                // Populate with data from the record
                command.Parameters.AddWithValue ("@COL1", ID);
                command.Parameters.AddWithValue ("@COL2", Name);
                command.Parameters.AddWithValue ("@COL3", Occupation);
                command.Parameters.AddWithValue ("@COL4", isManager);
                command.Parameters.AddWithValue ("@COL5", ManagerID);

                // Write to database
                command.ExecuteNonQuery ();
            }
            conn.Close ();

            // Save children to database as well
            for (nuint n = 0; n < People.Count; ++n) {
                // Grab person
                var Person = People.GetItem<PersonModel>(n);

                // Update sub record
                Person.Update (conn);
            }

            // Save last connection
            _conn = conn;
        }

        public void Load(SqliteConnection conn, string id) {
            bool shouldClose = false;

            // Clear last connection to prevent circular call to update
            _conn = null;

            // Is the database already open?
            if (conn.State != ConnectionState.Open) {
                shouldClose = true;
                conn.Open ();
            }

            // Execute query
            using (var command = conn.CreateCommand ()) {
                // Create new command
                command.CommandText = "SELECT * FROM [People] WHERE ID = @COL1";

                // Populate with data from the record
                command.Parameters.AddWithValue ("@COL1", id);

                using (var reader = command.ExecuteReader ()) {
                    while (reader.Read ()) {
                        // Pull values back into class
                        ID = (string)reader [0];
                        Name = (string)reader [1];
                        Occupation = (string)reader [2];
                        isManager = (bool)reader [3];
                        ManagerID = (string)reader [4];
                    }
                }
            }

            // Is this a manager?
            if (isManager) {
                // Yes, load children
                using (var command = conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = "SELECT ID FROM [People] WHERE ManagerID = @COL1";

                    // Populate with data from the record
                    command.Parameters.AddWithValue ("@COL1", id);

                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Load child and add to collection
                            var childID = (string)reader [0];
                            var person = new PersonModel (conn, childID);
                            _people.Add (person);
                        }
                    }
                }
            }

            // Should we close the connection to the database
            if (shouldClose) {
                conn.Close ();
            }

            // Save last connection
            _conn = conn;
        }

        public void Delete(SqliteConnection conn) {

            // Clear last connection to prevent circular call to update
            _conn = null;

            // Execute query
            conn.Open ();
            using (var command = conn.CreateCommand ()) {
                // Create new command
                command.CommandText = "DELETE FROM [People] WHERE (ID = @COL1 OR ManagerID = @COL1)";

                // Populate with data from the record
                command.Parameters.AddWithValue ("@COL1", ID);

                // Write to database
                command.ExecuteNonQuery ();
            }
            conn.Close ();

            // Empty class
            ID = "";
            ManagerID = "";
            Name = "";
            Occupation = "";
            isManager = false;
            _people = new NSMutableArray();

            // Save last connection
            _conn = conn;
        }
        #endregion
    }
}

Sehen wir uns die Änderungen im Detail unten an.

Zunächst haben wir mehrere using-Anweisungen hinzugefügt, die erforderlich sind, um SQLite zu verwenden, und wir haben eine Variable hinzugefügt, um unsere letzte Verbindung mit der SQLite-Datenbank zu speichern:

using System.Data;
using System.IO;
using Mono.Data.Sqlite;
...

private SqliteConnection _conn = null;

Wir verwenden diese gespeicherte Verbindung, um änderungen am Datensatz automatisch in der Datenbank zu speichern, wenn der Benutzer den Inhalt in der Benutzeroberfläche per Datenbindung ändert:

[Export("Name")]
public string Name {
    get { return _name; }
    set {
        WillChangeValue ("Name");
        _name = value;
        DidChangeValue ("Name");

        // Save changes to database?
        if (_conn != null) Update (_conn);
    }
}

[Export("Occupation")]
public string Occupation {
    get { return _occupation; }
    set {
        WillChangeValue ("Occupation");
        _occupation = value;
        DidChangeValue ("Occupation");

        // Save changes to database?
        if (_conn != null) Update (_conn);
    }
}

[Export("isManager")]
public bool isManager {
    get { return _isManager; }
    set {
        WillChangeValue ("isManager");
        WillChangeValue ("Icon");
        _isManager = value;
        DidChangeValue ("isManager");
        DidChangeValue ("Icon");

        // Save changes to database?
        if (_conn != null) Update (_conn);
    }
}

Alle Änderungen, die an den Eigenschaften Name, Beruf oder isManager vorgenommen wurden, werden an die Datenbank gesendet, wenn die Daten dort gespeichert wurden (z. B. wenn die _conn Variable nicht nullvorhanden ist). Als Nächstes befassen wir uns mit den Methoden, die wir zum Erstellen, Aktualisieren, Laden und Löschen von Personen aus der Datenbank hinzugefügt haben.

Neuen Datensatz erstellen

Der folgende Code wurde hinzugefügt, um einen neuen Datensatz in der SQLite-Datenbank zu erstellen:

public void Create(SqliteConnection conn) {

    // Clear last connection to prevent circular call to update
    _conn = null;

    // Create new record ID?
    if (ID == "") {
        ID = Guid.NewGuid ().ToString();
    }

    // Execute query
    conn.Open ();
    using (var command = conn.CreateCommand ()) {
        // Create new command
        command.CommandText = "INSERT INTO [People] (ID, Name, Occupation, isManager, ManagerID) VALUES (@COL1, @COL2, @COL3, @COL4, @COL5)";

        // Populate with data from the record
        command.Parameters.AddWithValue ("@COL1", ID);
        command.Parameters.AddWithValue ("@COL2", Name);
        command.Parameters.AddWithValue ("@COL3", Occupation);
        command.Parameters.AddWithValue ("@COL4", isManager);
        command.Parameters.AddWithValue ("@COL5", ManagerID);

        // Write to database
        command.ExecuteNonQuery ();
    }
    conn.Close ();

    // Save children to database as well
    for (nuint n = 0; n < People.Count; ++n) {
        // Grab person
        var Person = People.GetItem<PersonModel>(n);

        // Save manager ID and create the sub record
        Person.ManagerID = ID;
        Person.Create (conn);
    }

    // Save last connection
    _conn = conn;
}

Wir verwenden einen SQLiteCommand , um den neuen Datensatz in der Datenbank zu erstellen. Wir erhalten einen neuen Befehl aus dem SQLiteConnection (conn), den wir durch Aufrufen CreateCommandder Methode an die Methode übergeben haben. Als Nächstes legen wir die SQL-Anweisung so fest, dass der neue Datensatz tatsächlich geschrieben wird und Parameter für die tatsächlichen Werte bereitgestellt werden:

command.CommandText = "INSERT INTO [People] (ID, Name, Occupation, isManager, ManagerID) VALUES (@COL1, @COL2, @COL3, @COL4, @COL5)";

Später legen wir die Werte für die Parameter mithilfe der Parameters.AddWithValue Methode für die SQLiteCommand. Mithilfe von Parametern stellen wir sicher, dass Werte (z. B. ein einzelnes Anführungszeichen) ordnungsgemäß codiert werden, bevor sie an SQLite gesendet werden. Beispiel:

command.Parameters.AddWithValue ("@COL1", ID);

Da eine Person ein Vorgesetzter sein kann und eine Sammlung von Mitarbeitern darunter hat, rufen Create wir die Methode für diese Personen rekursiv auf, um sie auch in der Datenbank zu speichern:

// Save children to database as well
for (nuint n = 0; n < People.Count; ++n) {
    // Grab person
    var Person = People.GetItem<PersonModel>(n);

    // Save manager ID and create the sub record
    Person.ManagerID = ID;
    Person.Create (conn);
}

Aktualisieren eines Datensatzes

Der folgende Code wurde hinzugefügt, um einen vorhandenen Datensatz in der SQLite-Datenbank zu aktualisieren:

public void Update(SqliteConnection conn) {

    // Clear last connection to prevent circular call to update
    _conn = null;

    // Execute query
    conn.Open ();
    using (var command = conn.CreateCommand ()) {
        // Create new command
        command.CommandText = "UPDATE [People] SET Name = @COL2, Occupation = @COL3, isManager = @COL4, ManagerID = @COL5 WHERE ID = @COL1";

        // Populate with data from the record
        command.Parameters.AddWithValue ("@COL1", ID);
        command.Parameters.AddWithValue ("@COL2", Name);
        command.Parameters.AddWithValue ("@COL3", Occupation);
        command.Parameters.AddWithValue ("@COL4", isManager);
        command.Parameters.AddWithValue ("@COL5", ManagerID);

        // Write to database
        command.ExecuteNonQuery ();
    }
    conn.Close ();

    // Save children to database as well
    for (nuint n = 0; n < People.Count; ++n) {
        // Grab person
        var Person = People.GetItem<PersonModel>(n);

        // Update sub record
        Person.Update (conn);
    }

    // Save last connection
    _conn = conn;
}

Wie oben erstellen erhalten wir eine SQLiteCommand von der übergebenen SQLiteConnectionDaten und legen unsere SQL so fest, dass unser Datensatz aktualisiert wird (Parameter bereitstellen):

command.CommandText = "UPDATE [People] SET Name = @COL2, Occupation = @COL3, isManager = @COL4, ManagerID = @COL5 WHERE ID = @COL1";

Wir füllen die Parameterwerte aus (Beispiel: command.Parameters.AddWithValue ("@COL1", ID);) und erneut, rufen rekursiv aktualisierungen für alle untergeordneten Datensätze auf:

// Save children to database as well
for (nuint n = 0; n < People.Count; ++n) {
    // Grab person
    var Person = People.GetItem<PersonModel>(n);

    // Update sub record
    Person.Update (conn);
}

Laden eines Datensatzes

Der folgende Code wurde hinzugefügt, um einen vorhandenen Datensatz aus der SQLite-Datenbank zu laden:

public void Load(SqliteConnection conn, string id) {
    bool shouldClose = false;

    // Clear last connection to prevent circular call to update
    _conn = null;

    // Is the database already open?
    if (conn.State != ConnectionState.Open) {
        shouldClose = true;
        conn.Open ();
    }

    // Execute query
    using (var command = conn.CreateCommand ()) {
        // Create new command
        command.CommandText = "SELECT * FROM [People] WHERE ID = @COL1";

        // Populate with data from the record
        command.Parameters.AddWithValue ("@COL1", id);

        using (var reader = command.ExecuteReader ()) {
            while (reader.Read ()) {
                // Pull values back into class
                ID = (string)reader [0];
                Name = (string)reader [1];
                Occupation = (string)reader [2];
                isManager = (bool)reader [3];
                ManagerID = (string)reader [4];
            }
        }
    }

    // Is this a manager?
    if (isManager) {
        // Yes, load children
        using (var command = conn.CreateCommand ()) {
            // Create new command
            command.CommandText = "SELECT ID FROM [People] WHERE ManagerID = @COL1";

            // Populate with data from the record
            command.Parameters.AddWithValue ("@COL1", id);

            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Load child and add to collection
                    var childID = (string)reader [0];
                    var person = new PersonModel (conn, childID);
                    _people.Add (person);
                }
            }
        }
    }

    // Should we close the connection to the database
    if (shouldClose) {
        conn.Close ();
    }

    // Save last connection
    _conn = conn;
}

Da die Routine rekursiv von einem übergeordneten Objekt aufgerufen werden kann (z. B. ein Managerobjekt, das das Mitarbeiterobjekt lädt), wurde spezieller Code zum Behandeln des Öffnens und Schließens der Verbindung mit der Datenbank hinzugefügt:

bool shouldClose = false;
...

// Is the database already open?
if (conn.State != ConnectionState.Open) {
    shouldClose = true;
    conn.Open ();
}
...

// Should we close the connection to the database
if (shouldClose) {
    conn.Close ();
}

Wie immer legen wir unsere SQL so fest, dass der Datensatz abgerufen und Parameter verwendet werden:

// Create new command
command.CommandText = "SELECT ID FROM [People] WHERE ManagerID = @COL1";

// Populate with data from the record
command.Parameters.AddWithValue ("@COL1", id);

Schließlich verwenden wir einen Datenleser, um die Abfrage auszuführen und die Datensatzfelder zurückzugeben (die wir in die Instanz der PersonModel Klasse kopieren):

using (var reader = command.ExecuteReader ()) {
    while (reader.Read ()) {
        // Pull values back into class
        ID = (string)reader [0];
        Name = (string)reader [1];
        Occupation = (string)reader [2];
        isManager = (bool)reader [3];
        ManagerID = (string)reader [4];
    }
}

Wenn diese Person ein Vorgesetzter ist, müssen wir auch alle ihre Mitarbeiter laden (durch rekursives Aufrufen ihrer Load Methode):

// Is this a manager?
if (isManager) {
    // Yes, load children
    using (var command = conn.CreateCommand ()) {
        // Create new command
        command.CommandText = "SELECT ID FROM [People] WHERE ManagerID = @COL1";

        // Populate with data from the record
        command.Parameters.AddWithValue ("@COL1", id);

        using (var reader = command.ExecuteReader ()) {
            while (reader.Read ()) {
                // Load child and add to collection
                var childID = (string)reader [0];
                var person = new PersonModel (conn, childID);
                _people.Add (person);
            }
        }
    }
}

Löschen eines Datensatzes

Der folgende Code wurde hinzugefügt, um einen vorhandenen Datensatz aus der SQLite-Datenbank zu löschen:

public void Delete(SqliteConnection conn) {

    // Clear last connection to prevent circular call to update
    _conn = null;

    // Execute query
    conn.Open ();
    using (var command = conn.CreateCommand ()) {
        // Create new command
        command.CommandText = "DELETE FROM [People] WHERE (ID = @COL1 OR ManagerID = @COL1)";

        // Populate with data from the record
        command.Parameters.AddWithValue ("@COL1", ID);

        // Write to database
        command.ExecuteNonQuery ();
    }
    conn.Close ();

    // Empty class
    ID = "";
    ManagerID = "";
    Name = "";
    Occupation = "";
    isManager = false;
    _people = new NSMutableArray();

    // Save last connection
    _conn = conn;
}

Hier stellen wir die SQL bereit, um sowohl den Datensatz der Vorgesetzten als auch die Datensätze aller Mitarbeiter unter diesem Vorgesetzten zu löschen (mit Parametern):

// Create new command
command.CommandText = "DELETE FROM [People] WHERE (ID = @COL1 OR ManagerID = @COL1)";

// Populate with data from the record
command.Parameters.AddWithValue ("@COL1", ID);

Nachdem der Datensatz entfernt wurde, löschen wir die aktuelle Instanz der PersonModel Klasse:

// Empty class
ID = "";
ManagerID = "";
Name = "";
Occupation = "";
isManager = false;
_people = new NSMutableArray();

Initialisieren der Datenbank

Da die Änderungen an unserem Datenmodell vorhanden sind, um das Lesen und Schreiben in die Datenbank zu unterstützen, müssen wir eine Verbindung mit der Datenbank öffnen und bei der ersten Ausführung initialisieren. Fügen wir nun den folgenden Code zu unserer MainWindow.cs Datei hinzu:

using System.Data;
using System.IO;
using Mono.Data.Sqlite;
...

private SqliteConnection DatabaseConnection = null;
...

private SqliteConnection GetDatabaseConnection() {
    var documents = Environment.GetFolderPath (Environment.SpecialFolder.Desktop);
    string db = Path.Combine (documents, "People.db3");

    // Create the database if it doesn't already exist
    bool exists = File.Exists (db);
    if (!exists)
        SqliteConnection.CreateFile (db);

    // Create connection to the database
    var conn = new SqliteConnection("Data Source=" + db);

    // Set the structure of the database
    if (!exists) {
        var commands = new[] {
            "CREATE TABLE People (ID TEXT, Name TEXT, Occupation TEXT, isManager BOOLEAN, ManagerID TEXT)"
        };
        conn.Open ();
        foreach (var cmd in commands) {
            using (var c = conn.CreateCommand()) {
                c.CommandText = cmd;
                c.CommandType = CommandType.Text;
                c.ExecuteNonQuery ();
            }
        }
        conn.Close ();

        // Build list of employees
        var Craig = new PersonModel ("0","Craig Dunn", "Documentation Manager");
        Craig.AddPerson (new PersonModel ("Amy Burns", "Technical Writer"));
        Craig.AddPerson (new PersonModel ("Joel Martinez", "Web & Infrastructure"));
        Craig.AddPerson (new PersonModel ("Kevin Mullins", "Technical Writer"));
        Craig.AddPerson (new PersonModel ("Mark McLemore", "Technical Writer"));
        Craig.AddPerson (new PersonModel ("Tom Opgenorth", "Technical Writer"));
        Craig.Create (conn);

        var Larry = new PersonModel ("1","Larry O'Brien", "API Documentation Manager");
        Larry.AddPerson (new PersonModel ("Mike Norman", "API Documentor"));
        Larry.Create (conn);
    }

    // Return new connection
    return conn;
}

Sehen wir uns den obigen Code genauer an. Zuerst wählen wir einen Speicherort für die neue Datenbank (in diesem Beispiel desktop des Benutzers) aus, um festzustellen, ob die Datenbank vorhanden ist, und falls dies nicht der Fall ist, erstellen Sie ihn:

var documents = Environment.GetFolderPath (Environment.SpecialFolder.Desktop);
string db = Path.Combine (documents, "People.db3");

// Create the database if it doesn't already exist
bool exists = File.Exists (db);
if (!exists)
    SqliteConnection.CreateFile (db);

Als Nächstes richten wir die Verbindung mit der Datenbank mithilfe des oben erstellten Pfads ein:

var conn = new SqliteConnection("Data Source=" + db);

Anschließend erstellen wir alle SQL-Tabellen in der Datenbank, die wir benötigen:

var commands = new[] {
    "CREATE TABLE People (ID TEXT, Name TEXT, Occupation TEXT, isManager BOOLEAN, ManagerID TEXT)"
};
conn.Open ();
foreach (var cmd in commands) {
    using (var c = conn.CreateCommand()) {
        c.CommandText = cmd;
        c.CommandType = CommandType.Text;
        c.ExecuteNonQuery ();
    }
}
conn.Close ();

Schließlich verwenden wir unser Datenmodell (PersonModel), um einen Standardsatz von Datensätzen für die Datenbank zu erstellen, wenn die Anwendung zum ersten Mal ausgeführt wird oder wenn die Datenbank fehlt:

// Build list of employees
var Craig = new PersonModel ("0","Craig Dunn", "Documentation Manager");
Craig.AddPerson (new PersonModel ("Amy Burns", "Technical Writer"));
Craig.AddPerson (new PersonModel ("Joel Martinez", "Web & Infrastructure"));
Craig.AddPerson (new PersonModel ("Kevin Mullins", "Technical Writer"));
Craig.AddPerson (new PersonModel ("Mark McLemore", "Technical Writer"));
Craig.AddPerson (new PersonModel ("Tom Opgenorth", "Technical Writer"));
Craig.Create (conn);

var Larry = new PersonModel ("1","Larry O'Brien", "API Documentation Manager");
Larry.AddPerson (new PersonModel ("Mike Norman", "API Documentor"));
Larry.Create (conn);

Wenn die Anwendung gestartet und das Hauptfenster geöffnet wird, stellen wir mit dem oben hinzugefügten Code eine Verbindung mit der Datenbank bereit:

public override void AwakeFromNib ()
{
    base.AwakeFromNib ();

    // Get access to database
    DatabaseConnection = GetDatabaseConnection ();
}

Laden gebundener Daten

Da alle Komponenten für den direkten Zugriff auf gebundene Daten aus einer SQLite-Datenbank vorhanden sind, können wir die Daten in den verschiedenen Ansichten laden, die unsere Anwendung bereitstellt, und sie wird automatisch in unserer Benutzeroberfläche angezeigt.

Laden eines einzelnen Datensatzes

Um einen einzelnen Datensatz zu laden, in dem die ID bekannt ist, können wir den folgenden Code verwenden:

Person = new PersonModel (Conn, "0");

Alle Datensätze werden geladen.

Um alle Personen zu laden, unabhängig davon, ob sie ein Manager sind oder nicht, verwenden Sie den folgenden Code:

// Load all employees
_conn.Open ();
using (var command = _conn.CreateCommand ()) {
    // Create new command
    command.CommandText = "SELECT ID FROM [People]";

    using (var reader = command.ExecuteReader ()) {
        while (reader.Read ()) {
            // Load child and add to collection
            var childID = (string)reader [0];
            var person = new PersonModel (_conn, childID);
            AddPerson (person);
        }
    }
}
_conn.Close ();

Hier verwenden wir eine Überladung des Konstruktors für die Klasse, um die PersonModel Person in den Arbeitsspeicher zu laden:

var person = new PersonModel (_conn, childID);

Wir rufen auch die Data Bound-Klasse auf, um die Person zu unserer Sammlung von Personen AddPerson (person)hinzuzufügen. Dadurch wird sichergestellt, dass unsere Benutzeroberfläche die Änderung erkennt und anzeigt:

[Export("addObject:")]
public void AddPerson(PersonModel person) {
    WillChangeValue ("personModelArray");
    isManager = true;
    _people.Add (person);
    DidChangeValue ("personModelArray");
}

Nur Datensätze der obersten Ebene werden geladen

Zum Laden von Managern (z. B. zum Anzeigen von Daten in einer Gliederungsansicht) verwenden wir den folgenden Code:

// Load only managers employees
_conn.Open ();
using (var command = _conn.CreateCommand ()) {
    // Create new command
    command.CommandText = "SELECT ID FROM [People] WHERE isManager = 1";

    using (var reader = command.ExecuteReader ()) {
        while (reader.Read ()) {
            // Load child and add to collection
            var childID = (string)reader [0];
            var person = new PersonModel (_conn, childID);
            AddPerson (person);
        }
    }
}
_conn.Close ();

Der einzige echte Unterschied in der SQL-Anweisung (die nur Manager command.CommandText = "SELECT ID FROM [People] WHERE isManager = 1"lädt), funktioniert aber genauso wie der obige Abschnitt.

Datenbanken und Kombinationsboxen

Die menüsteuerelemente, die für macOS (z. B. das Kombinationsfeld) verfügbar sind, können so festgelegt werden, dass die Dropdownliste entweder aus einer internen Liste (die im Schnittstellen-Generator vordefiniert oder über Code aufgefüllt werden kann) oder indem Sie eine eigene benutzerdefinierte, externe Datenquelle bereitstellen. Weitere Details finden Sie unter Bereitstellen von Menüsteuerelementdaten .

Bearbeiten Sie beispielsweise das obige Beispiel für einfache Bindung im Schnittstellen-Generator, fügen Sie ein Kombinationsfeld hinzu, und machen Sie es mit einer Steckdose mit dem Namen verfügbar EmployeeSelector:

Exposing a combo box outlet

Überprüfen Sie im Attributes Inspector die Eigenschaften "AutoVervollständigen " und verwendet Die Datenquelleneigenschaften :

Configuring the combo box attributes

Speichern Sie Ihre Änderungen, und kehren Sie zum synchronisierenden Visual Studio für Mac zurück.

Bereitstellen von Kombinationsfelddaten

Fügen Sie als Nächstes eine neue Klasse zum aufgerufenen ComboBoxDataSource Projekt hinzu, und sehen Sie wie folgt aus:

using System;
using System.Data;
using System.IO;
using Mono.Data.Sqlite;
using Foundation;
using AppKit;

namespace MacDatabase
{
    public class ComboBoxDataSource : NSComboBoxDataSource
    {
        #region Private Variables
        private SqliteConnection _conn = null;
        private string _tableName = "";
        private string _IDField = "ID";
        private string _displayField = "";
        private nint _recordCount = 0;
        #endregion

        #region Computed Properties
        public SqliteConnection Conn {
            get { return _conn; }
            set { _conn = value; }
        }

        public string TableName {
            get { return _tableName; }
            set { 
                _tableName = value;
                _recordCount = GetRecordCount ();
            }
        }

        public string IDField {
            get { return _IDField; }
            set {
                _IDField = value; 
                _recordCount = GetRecordCount ();
            }
        }

        public string DisplayField {
            get { return _displayField; }
            set { 
                _displayField = value; 
                _recordCount = GetRecordCount ();
            }
        }

        public nint RecordCount {
            get { return _recordCount; }
        }
        #endregion

        #region Constructors
        public ComboBoxDataSource (SqliteConnection conn, string tableName, string displayField)
        {
            // Initialize
            this.Conn = conn;
            this.TableName = tableName;
            this.DisplayField = displayField;
        }

        public ComboBoxDataSource (SqliteConnection conn, string tableName, string idField, string displayField)
        {
            // Initialize
            this.Conn = conn;
            this.TableName = tableName;
            this.IDField = idField;
            this.DisplayField = displayField;
        }
        #endregion

        #region Private Methods
        private nint GetRecordCount ()
        {
            bool shouldClose = false;
            nint count = 0;

            // Has a Table, ID and display field been specified?
            if (TableName !="" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT count({IDField}) FROM [{TableName}]";

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read count from query
                            var result = (long)reader [0];
                            count = (nint)result;
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return the number of records
            return count;
        }
        #endregion

        #region Public Methods
        public string IDForIndex (nint index)
        {
            NSString value = new NSString ("");
            bool shouldClose = false;

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {IDField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read the display field from the query
                            value = new NSString ((string)reader [0]);
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return value;
        }

        public string ValueForIndex (nint index)
        {
            NSString value = new NSString ("");
            bool shouldClose = false;

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read the display field from the query
                            value = new NSString ((string)reader [0]);
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return value;
        }

        public string IDForValue (string value)
        {
            NSString result = new NSString ("");
            bool shouldClose = false;

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {IDField} FROM [{TableName}] WHERE {DisplayField} = @VAL";

                    // Populate parameters
                    command.Parameters.AddWithValue ("@VAL", value);

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read the display field from the query
                            result = new NSString ((string)reader [0]);
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return result;
        }
        #endregion 

        #region Override Methods
        public override nint ItemCount (NSComboBox comboBox)
        {
            return RecordCount;
        }

        public override NSObject ObjectValueForItem (NSComboBox comboBox, nint index)
        {
            NSString value = new NSString ("");
            bool shouldClose = false;

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read the display field from the query
                            value = new NSString((string)reader [0]);
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return value;
        }

        public override nint IndexOfItem (NSComboBox comboBox, string value)
        {
            bool shouldClose = false;
            bool found = false;
            string field = "";
            nint index = NSRange.NotFound;

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC";

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read () && !found) {
                            // Read the display field from the query
                            field = (string)reader [0];
                            ++index;

                            // Is this the value we are searching for?
                            if (value == field) {
                                // Yes, exit loop
                                found = true;
                            }
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return index;
        }

        public override string CompletedString (NSComboBox comboBox, string uncompletedString)
        {
            bool shouldClose = false;
            bool found = false;
            string field = "";

            // Has a Table, ID and display field been specified?
            if (TableName != "" && IDField != "" && DisplayField != "") {
                // Is the database already open?
                if (Conn.State != ConnectionState.Open) {
                    shouldClose = true;
                    Conn.Open ();
                }

                // Escape search string
                uncompletedString = uncompletedString.Replace ("'", "");

                // Execute query
                using (var command = Conn.CreateCommand ()) {
                    // Create new command
                    command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] WHERE {DisplayField} LIKE @VAL";

                    // Populate parameters
                    command.Parameters.AddWithValue ("@VAL", uncompletedString + "%");

                    // Get the results from the database
                    using (var reader = command.ExecuteReader ()) {
                        while (reader.Read ()) {
                            // Read the display field from the query
                            field = (string)reader [0];
                        }
                    }
                }

                // Should we close the connection to the database
                if (shouldClose) {
                    Conn.Close ();
                }
            }

            // Return results
            return field;
        }
        #endregion
    }
}

In diesem Beispiel erstellen wir eine neue NSComboBoxDataSource , die Kombinationsfeldelemente aus einer beliebigen SQLite-Datenquelle darstellen kann. Zunächst definieren wir die folgenden Eigenschaften:

  • Conn – Ruft eine Verbindung mit der SQLite-Datenbank ab oder legt sie fest.
  • TableName - Ruft den Tabellennamen ab oder legt den Namen fest.
  • IDField - Ruft das Feld ab, das die eindeutige ID für die angegebene Tabelle bereitstellt, oder legt dieses fest. Der Standardwert ist ID.
  • DisplayField – Ruft das Feld ab, das in der Dropdownliste angezeigt wird, oder legt es fest.
  • RecordCount - Ruft die Anzahl der Datensätze in der angegebenen Tabelle ab.

Wenn wir eine neue Instanz des Objekts erstellen, übergeben wir die Verbindung, den Tabellennamen, optional das ID-Feld und das Anzeigefeld:

public ComboBoxDataSource (SqliteConnection conn, string tableName, string displayField)
{
    // Initialize
    this.Conn = conn;
    this.TableName = tableName;
    this.DisplayField = displayField;
}

Die GetRecordCount Methode gibt die Anzahl der Datensätze in der angegebenen Tabelle zurück:

private nint GetRecordCount ()
{
    bool shouldClose = false;
    nint count = 0;

    // Has a Table, ID and display field been specified?
    if (TableName !="" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT count({IDField}) FROM [{TableName}]";

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read count from query
                    var result = (long)reader [0];
                    count = (nint)result;
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return the number of records
    return count;
}

Sie wird immer dann aufgerufen, IDField wenn der Wert der TableNameDisplayField Eigenschaften geändert wird.

Die IDForIndex Methode gibt die eindeutige ID (IDField) für den Datensatz im angegebenen Dropdownlistenelementindex zurück:

public string IDForIndex (nint index)
{
    NSString value = new NSString ("");
    bool shouldClose = false;

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {IDField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read the display field from the query
                    value = new NSString ((string)reader [0]);
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return value;
}

Die ValueForIndex Methode gibt den Wert (DisplayField) für das Element im angegebenen Dropdownlistenindex zurück:

public string ValueForIndex (nint index)
{
    NSString value = new NSString ("");
    bool shouldClose = false;

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read the display field from the query
                    value = new NSString ((string)reader [0]);
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return value;
}

Die IDForValue Methode gibt die eindeutige ID () für den angegebenen Wert zurückIDField (DisplayField):

public string IDForValue (string value)
{
    NSString result = new NSString ("");
    bool shouldClose = false;

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {IDField} FROM [{TableName}] WHERE {DisplayField} = @VAL";

            // Populate parameters
            command.Parameters.AddWithValue ("@VAL", value);

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read the display field from the query
                    result = new NSString ((string)reader [0]);
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return result;
}

Gibt ItemCount die vorkompilierte Anzahl von Elementen in der Liste zurück, wie berechnet, wenn die TableNameEigenschaften IDFieldDisplayField geändert werden:

public override nint ItemCount (NSComboBox comboBox)
{
    return RecordCount;
}

Die ObjectValueForItem Methode stellt den Wert (DisplayField) für den angegebenen Dropdownlistenelementindex bereit:

public override NSObject ObjectValueForItem (NSComboBox comboBox, nint index)
{
    NSString value = new NSString ("");
    bool shouldClose = false;

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC LIMIT 1 OFFSET {index}";

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read the display field from the query
                    value = new NSString((string)reader [0]);
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return value;
}

Beachten Sie, dass wir die Anweisungen und OFFSET Anweisungen LIMIT in unserem SQLite-Befehl verwenden, um auf den einzigen Datensatz zu beschränken, den wir benötigen.

Die IndexOfItem Methode gibt den Dropdownelementindex des angegebenen Werts (DisplayField) zurück:

public override nint IndexOfItem (NSComboBox comboBox, string value)
{
    bool shouldClose = false;
    bool found = false;
    string field = "";
    nint index = NSRange.NotFound;

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] ORDER BY {DisplayField} ASC";

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read () && !found) {
                    // Read the display field from the query
                    field = (string)reader [0];
                    ++index;

                    // Is this the value we are searching for?
                    if (value == field) {
                        // Yes, exit loop
                        found = true;
                    }
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return index;
}

Wenn der Wert nicht gefunden werden kann, wird der NSRange.NotFound Wert zurückgegeben, und alle Elemente werden in der Dropdownliste deaktiviert.

Die CompletedString Methode gibt den ersten übereinstimmenden Wert (DisplayField) für einen teilweise eingegebenen Eintrag zurück:

public override string CompletedString (NSComboBox comboBox, string uncompletedString)
{
    bool shouldClose = false;
    bool found = false;
    string field = "";

    // Has a Table, ID and display field been specified?
    if (TableName != "" && IDField != "" && DisplayField != "") {
        // Is the database already open?
        if (Conn.State != ConnectionState.Open) {
            shouldClose = true;
            Conn.Open ();
        }

        // Escape search string
        uncompletedString = uncompletedString.Replace ("'", "");

        // Execute query
        using (var command = Conn.CreateCommand ()) {
            // Create new command
            command.CommandText = $"SELECT {DisplayField} FROM [{TableName}] WHERE {DisplayField} LIKE @VAL";

            // Populate parameters
            command.Parameters.AddWithValue ("@VAL", uncompletedString + "%");

            // Get the results from the database
            using (var reader = command.ExecuteReader ()) {
                while (reader.Read ()) {
                    // Read the display field from the query
                    field = (string)reader [0];
                }
            }
        }

        // Should we close the connection to the database
        if (shouldClose) {
            Conn.Close ();
        }
    }

    // Return results
    return field;
}

Anzeigen von Daten und Reagieren auf Ereignisse

Wenn Sie alle Teile zusammenführen möchten, bearbeiten Sie sie SubviewSimpleBindingController , und gestalten Sie sie wie folgt:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.IO;
using Mono.Data.Sqlite;
using Foundation;
using AppKit;

namespace MacDatabase
{
    public partial class SubviewSimpleBindingController : AppKit.NSViewController
    {
        #region Private Variables
        private PersonModel _person = new PersonModel();
        private SqliteConnection Conn;
        #endregion

        #region Computed Properties
        //strongly typed view accessor
        public new SubviewSimpleBinding View {
            get {
                return (SubviewSimpleBinding)base.View;
            }
        }

        [Export("Person")]
        public PersonModel Person {
            get {return _person; }
            set {
                WillChangeValue ("Person");
                _person = value;
                DidChangeValue ("Person");
            }
        }

        public ComboBoxDataSource DataSource {
            get { return EmployeeSelector.DataSource as ComboBoxDataSource; }
        }
        #endregion

        #region Constructors
        // Called when created from unmanaged code
        public SubviewSimpleBindingController (IntPtr handle) : base (handle)
        {
            Initialize ();
        }

        // Called when created directly from a XIB file
        [Export ("initWithCoder:")]
        public SubviewSimpleBindingController (NSCoder coder) : base (coder)
        {
            Initialize ();
        }

        // Call to load from the XIB/NIB file
        public SubviewSimpleBindingController (SqliteConnection conn) : base ("SubviewSimpleBinding", NSBundle.MainBundle)
        {
            // Initialize
            this.Conn = conn;
            Initialize ();
        }

        // Shared initialization code
        void Initialize ()
        {
        }
        #endregion

        #region Private Methods
        private void LoadSelectedPerson (string id)
        {

            // Found?
            if (id != "") {
                // Yes, load requested record
                Person = new PersonModel (Conn, id);
            }
        }
        #endregion

        #region Override Methods
        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();

            // Configure Employee selector dropdown
            EmployeeSelector.DataSource = new ComboBoxDataSource (Conn, "People", "Name");

            // Wireup events
            EmployeeSelector.Changed += (sender, e) => {
                // Get ID
                var id = DataSource.IDForValue (EmployeeSelector.StringValue);
                LoadSelectedPerson (id);
            };

            EmployeeSelector.SelectionChanged += (sender, e) => {
                // Get ID
                var id = DataSource.IDForIndex (EmployeeSelector.SelectedIndex);
                LoadSelectedPerson (id);
            };

            // Auto select the first person
            EmployeeSelector.StringValue = DataSource.ValueForIndex (0);
            Person = new PersonModel (Conn, DataSource.IDForIndex(0));
    
        }
        #endregion
    }
}

Die DataSource Eigenschaft stellt eine Verknüpfung mit dem ComboBoxDataSource oben erstellten Kombinationsfeld bereit.

Die LoadSelectedPerson Methode lädt die Person aus der Datenbank für die angegebene eindeutige ID:

private void LoadSelectedPerson (string id)
{

    // Found?
    if (id != "") {
        // Yes, load requested record
        Person = new PersonModel (Conn, id);
    }
}

Bei der AwakeFromNib Methodenüberschreibung fügen wir zuerst eine Instanz unserer benutzerdefinierten Kombinationsfeld-Datenquelle an:

EmployeeSelector.DataSource = new ComboBoxDataSource (Conn, "People", "Name");

Als Nächstes antworten wir darauf, dass der Benutzer den Textwert des Kombinationsfelds bearbeitet, indem wir die zugeordnete eindeutige ID (IDField) der Daten suchen, die präsentiert werden, und die angegebene Person wird geladen, wenn sie gefunden wurde:

EmployeeSelector.Changed += (sender, e) => {
    // Get ID
    var id = DataSource.IDForValue (EmployeeSelector.StringValue);
    LoadSelectedPerson (id);
};

Außerdem wird eine neue Person geladen, wenn der Benutzer ein neues Element aus der Dropdownliste auswählt:

EmployeeSelector.SelectionChanged += (sender, e) => {
    // Get ID
    var id = DataSource.IDForIndex (EmployeeSelector.SelectedIndex);
    LoadSelectedPerson (id);
};

Schließlich füllen wir das Kombinationsfeld automatisch auf und zeigen die Person mit dem ersten Element in der Liste an:

// Auto select the first person
EmployeeSelector.StringValue = DataSource.ValueForIndex (0);
Person = new PersonModel (Conn, DataSource.IDForIndex(0));

SQLite.NET ORM

Wie bereits erwähnt, können wir mit dem Open Source-SQLite.NET Object Relationship Manager (ORM) die Menge an Code erheblich reduzieren, der zum Lesen und Schreiben von Daten aus einer SQLite-Datenbank erforderlich ist. Dies ist möglicherweise nicht die beste Route, die beim Binden von Daten verwendet werden kann, da mehrere Anforderungen, die die Schlüsselwertcodierung und Datenbindung auf einem Objekt platzieren.

Laut der SQLite.Net Website ist SQLite eine Softwarebibliothek, die ein eigenständiges, serverloses, nullkonfigurationsloses, transaktionsbasiertes SQL-Datenbankmodul implementiert. SQLite ist das am häufigsten bereitgestellte Datenbankmodul der Welt. Der Quellcode für SQLite befindet sich in der öffentlichen Do Standard."

In den folgenden Abschnitten wird gezeigt, wie Sie SQLite.Net verwenden, um Daten für eine Tabellenansicht bereitzustellen.

Einschließen des SQLite.net NuGet

SQLite.NET wird als NuGet-Paket dargestellt, das Sie in Ihre Anwendung einschließen. Bevor wir mithilfe von SQLite.NET Datenbankunterstützung hinzufügen können, müssen wir dieses Paket einschließen.

Gehen Sie wie folgt vor, um das Paket hinzuzufügen:

  1. Klicken Sie im Lösungskreuz mit der rechten Maustaste auf den Ordner "Pakete", und wählen Sie "Pakete hinzufügen" aus...

  2. Geben Sie SQLite.net das Suchfeld ein, und wählen Sie den sqlite-net-Eintrag aus:

    Adding the SQLite NuGet package

  3. Klicken Sie auf die Schaltfläche "Paket hinzufügen", um den Vorgang abzuschließen.

Erstellen des Datenmodells

Fügen wir dem Projekt eine neue Klasse hinzu und rufen sie an OccupationModel. Als Nächstes bearbeiten wir die OccupationModel.cs Datei und lassen Sie sie wie folgt aussehen:

using System;
using SQLite;

namespace MacDatabase
{
    public class OccupationModel
    {
        #region Computed Properties
        [PrimaryKey, AutoIncrement]
        public int ID { get; set; }

        public string Name { get; set;}
        public string Description { get; set;}
        #endregion

        #region Constructors
        public OccupationModel ()
        {
        }

        public OccupationModel (string name, string description)
        {

            // Initialize
            this.Name = name;
            this.Description = description;

        }
        #endregion
    }
}

Zunächst schließen wir SQLite.NET (using Sqlite) ein und stellen dann mehrere Eigenschaften zur Verfügung, von denen jedes in die Datenbank geschrieben wird, wenn dieser Datensatz gespeichert wird. Die erste Eigenschaft, die wir als Primärschlüssel erstellen und festlegen, dass sie wie folgt automatisch erhöht wird:

[PrimaryKey, AutoIncrement]
public int ID { get; set; }

Initialisieren der Datenbank

Da die Änderungen an unserem Datenmodell vorhanden sind, um das Lesen und Schreiben in die Datenbank zu unterstützen, müssen wir eine Verbindung mit der Datenbank öffnen und bei der ersten Ausführung initialisieren. Fügen wir den folgenden Code hinzu:

using SQLite;
...

public SQLiteConnection Conn { get; set; }
...

private SQLiteConnection GetDatabaseConnection() {
    var documents = Environment.GetFolderPath (Environment.SpecialFolder.Desktop);
    string db = Path.Combine (documents, "Occupation.db3");
    OccupationModel Occupation;

    // Create the database if it doesn't already exist
    bool exists = File.Exists (db);

    // Create connection to database
    var conn = new SQLiteConnection (db);

    // Initially populate table?
    if (!exists) {
        // Yes, build table
        conn.CreateTable<OccupationModel> ();

        // Add occupations
        Occupation = new OccupationModel ("Documentation Manager", "Manages the Documentation Group");
        conn.Insert (Occupation);

        Occupation = new OccupationModel ("Technical Writer", "Writes technical documentation and sample applications");
        conn.Insert (Occupation);

        Occupation = new OccupationModel ("Web & Infrastructure", "Creates and maintains the websites that drive documentation");
        conn.Insert (Occupation);

        Occupation = new OccupationModel ("API Documentation Manager", "Manages the API Documentation Group");
        conn.Insert (Occupation);

        Occupation = new OccupationModel ("API Documenter", "Creates and maintains API documentation");
        conn.Insert (Occupation);
    }

    return conn;
}

Zunächst wird ein Pfad zur Datenbank (in diesem Fall desktop des Benutzers) abgerufen und überprüft, ob die Datenbank bereits vorhanden ist:

var documents = Environment.GetFolderPath (Environment.SpecialFolder.Desktop);
string db = Path.Combine (documents, "Occupation.db3");
OccupationModel Occupation;

// Create the database if it doesn't already exist
bool exists = File.Exists (db);

Als Nächstes richten wir eine Verbindung mit der Datenbank am oben erstellten Pfad ein:

var conn = new SQLiteConnection (db);

Schließlich erstellen wir die Tabelle und fügen einige Standardeinträge hinzu:

// Yes, build table
conn.CreateTable<OccupationModel> ();

// Add occupations
Occupation = new OccupationModel ("Documentation Manager", "Manages the Documentation Group");
conn.Insert (Occupation);

Occupation = new OccupationModel ("Technical Writer", "Writes technical documentation and sample applications");
conn.Insert (Occupation);

Occupation = new OccupationModel ("Web & Infrastructure", "Creates and maintains the websites that drive documentation");
conn.Insert (Occupation);

Occupation = new OccupationModel ("API Documentation Manager", "Manages the API Documentation Group");
conn.Insert (Occupation);

Occupation = new OccupationModel ("API Documenter", "Creates and maintains API documentation");
conn.Insert (Occupation);

Hinzufügen einer Tabellenansicht

Als Beispiel für die Verwendung fügen wir unserer Benutzeroberfläche im Benutzeroberflächen-Generator von Xcode eine Tabellenansicht hinzu. Wir stellen diese Tabellenansicht über eine Steckdose (OccupationTable) zur Verfügung, damit wir über C#-Code darauf zugreifen können:

Exposing a table view outlet

Als Nächstes fügen wir die benutzerdefinierten Klassen hinzu, um diese Tabelle mit Daten aus der SQLite.NET-Datenbank aufzufüllen.

Erstellen der Tabellendatenquelle

Erstellen wir nun eine benutzerdefinierte Datenquelle, um Daten für unsere Tabelle bereitzustellen. Fügen Sie zunächst eine neue Klasse hinzu, die aufgerufen wird TableORMDatasource , und lassen Sie sie wie folgt aussehen:

using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;
using SQLite;

namespace MacDatabase
{
    public class TableORMDatasource : NSTableViewDataSource
    {
        #region Computed Properties
        public List<OccupationModel> Occupations { get; set;} = new List<OccupationModel>();
        public SQLiteConnection Conn { get; set; }
        #endregion

        #region Constructors
        public TableORMDatasource (SQLiteConnection conn)
        {
            // Initialize
            this.Conn = conn;
            LoadOccupations ();
        }
        #endregion

        #region Public Methods
        public void LoadOccupations() {

            // Get occupations from database
            var query = Conn.Table<OccupationModel> ();

            // Copy into table collection
            Occupations.Clear ();
            foreach (OccupationModel occupation in query) {
                Occupations.Add (occupation);
            }

        }
        #endregion

        #region Override Methods
        public override nint GetRowCount (NSTableView tableView)
        {
            return Occupations.Count;
        }
        #endregion
    }
}

Wenn wir später eine Instanz dieser Klasse erstellen, übergeben wir unsere geöffnete SQLite.NET Datenbankverbindung. Die LoadOccupations Methode fragt die Datenbank ab und kopiert die gefundenen Datensätze in den Arbeitsspeicher (mit unserem OccupationModel Datenmodell).

Erstellen des Tabellendelegats

Die letzte Klasse, die wir benötigen, ist ein benutzerdefinierter Tabellendelegat, um die Informationen anzuzeigen, die wir aus der SQLite.NET-Datenbank geladen haben. Fügen wir ein neues TableORMDelegate Zu unserem Projekt hinzu und lassen Sie es wie folgt aussehen:

using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;
using SQLite;

namespace MacDatabase
{
    public class TableORMDelegate : NSTableViewDelegate
    {
        #region Constants 
        private const string CellIdentifier = "OccCell";
        #endregion

        #region Private Variables
        private TableORMDatasource DataSource;
        #endregion

        #region Constructors
        public TableORMDelegate (TableORMDatasource dataSource)
        {
            // Initialize
            this.DataSource = dataSource;
        }
        #endregion

        #region Override Methods
        public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
        {
            // This pattern allows you reuse existing views when they are no-longer in use.
            // If the returned view is null, you instance up a new view
            // If a non-null view is returned, you modify it enough to reflect the new data
            NSTextField view = (NSTextField)tableView.MakeView (CellIdentifier, this);
            if (view == null) {
                view = new NSTextField ();
                view.Identifier = CellIdentifier;
                view.BackgroundColor = NSColor.Clear;
                view.Bordered = false;
                view.Selectable = false;
                view.Editable = false;
            }

            // Setup view based on the column selected
            switch (tableColumn.Title) {
            case "Occupation":
                view.StringValue = DataSource.Occupations [(int)row].Name;
                break;
            case "Description":
                view.StringValue = DataSource.Occupations [(int)row].Description;
                break;
            }

            return view;
        }
        #endregion
    }
}

Hier verwenden wir die Sammlung der Datenquelle Occupations (die wir aus der SQLite.NET-Datenbank geladen haben), um die Spalten unserer Tabelle über die GetViewForItem Methodenüberschreibung auszufüllen.

Auffüllen der Tabelle

Wenn alle Teile vorhanden sind, füllen wir unsere Tabelle auf, wenn sie aus der XIB-Datei aufgeblasen wird, indem wir die AwakeFromNib Methode überschreiben und so aussehen lassen:

public override void AwakeFromNib ()
{
    base.AwakeFromNib ();

    // Get database connection
    Conn = GetDatabaseConnection ();

    // Create the Occupation Table Data Source and populate it
    var DataSource = new TableORMDatasource (Conn);

    // Populate the Product Table
    OccupationTable.DataSource = DataSource;
    OccupationTable.Delegate = new TableORMDelegate (DataSource);
}

Zunächst erhalten wir Zugriff auf unsere SQLite.NET-Datenbank, erstellen und auffüllen, wenn sie noch nicht vorhanden ist. Als Nächstes erstellen wir eine neue Instanz unserer benutzerdefinierten Tabellendatenquelle, übergeben unsere Datenbankverbindung und fügen sie an die Tabelle an. Schließlich erstellen wir eine neue Instanz unseres benutzerdefinierten Tabellendelegats, übergeben unsere Datenquelle und fügen sie an die Tabelle an.

Zusammenfassung

In diesem Artikel wird die Arbeit mit datenbindung und Key-Value-Codierung mit SQLite-Datenbanken in einer Xamarin.Mac-Anwendung ausführlich erläutert. Zunächst wurde eine C#-Klasse Objective-C mithilfe von Schlüsselwertcodierung (Key-Value Coding, KVC) und Key-Value Observing (KVO) bereitgestellt. Als Nächstes wurde gezeigt, wie sie eine KVO-kompatible Klasse und Datenbindung an UI-Elemente im Schnittstellen-Generator von Xcode verwenden. Der Artikel behandelte auch das Arbeiten mit SQLite-Daten über das SQLite.NET ORM und das Anzeigen dieser Daten in einer Tabellenansicht.