Udostępnij za pośrednictwem


Bazy danych na platformie Xamarin.Mac

W tym artykule opisano używanie kodowania klucz-wartość i obserwowania wartości klucza, aby umożliwić powiązanie danych między bazami danych SQLite i elementami interfejsu użytkownika w narzędziu Interface Builder programu Xcode. Obejmuje również korzystanie z SQLite.NET ORM w celu zapewnienia dostępu do danych SQLite.

Omówienie

Podczas pracy z językami C# i .NET w aplikacji platformy Xamarin.Mac masz dostęp do tych samych baz danych SQLite, do których może uzyskać dostęp aplikacja platformy Xamarin.iOS lub Xamarin.Android.

W tym artykule omówimy dwa sposoby uzyskiwania dostępu do danych SQLite:

  1. Bezpośredni dostęp — bezpośrednio korzystając z bazy danych SQLite, możemy użyć danych z bazy danych do kodowania klucz-wartość i powiązania danych z elementami interfejsu użytkownika utworzonymi w narzędziu Interface Builder programu Xcode. Korzystając z technik kodowania klucz-wartość i powiązania danych w aplikacji Xamarin.Mac, możesz znacznie zmniejszyć ilość kodu, który trzeba napisać i obsługiwać, aby wypełnić elementy interfejsu użytkownika i pracować z elementami interfejsu użytkownika. Masz również korzyść z dalszego oddzielenia danych zapasowych (modelu danych) od interfejsu użytkownika frontonu (Model-View-Controller), co prowadzi do łatwiejszego utrzymania, bardziej elastycznego projektowania aplikacji.
  2. SQLite.NET ORM — korzystając z menedżera relacji obiektów typu open source (ORM) SQLite.NET typu open source, możemy znacznie zmniejszyć ilość kodu wymaganego do odczytu i zapisu danych z bazy danych SQLite. Te dane mogą być następnie używane do wypełniania elementu interfejsu użytkownika, takiego jak widok tabeli.

Przykład uruchomionej aplikacji

W tym artykule omówimy podstawy pracy z kodowaniem klucz-wartość i powiązaniem danych z bazami danych SQLite w aplikacji Xamarin.Mac. Zdecydowanie zaleca się, aby najpierw zapoznać się z artykułem Hello, Mac , w szczególności wprowadzenie do narzędzi Xcode i Interface Builder i Outlet and Actions , ponieważ obejmuje ona kluczowe pojęcia i techniki, których będziemy używać w tym artykule.

Ponieważ będziemy używać kodowania klucz-wartość i powiązania danych, najpierw przepracuj powiązanie danych i kodowanie klucz-wartość, ponieważ zostaną omówione podstawowe techniki i pojęcia, które będą używane w tej dokumentacji i jej przykładowej aplikacji.

Warto zapoznać się z sekcją Uwidacznianie klas/metod Objective-C języka C# w sekcji dokumentu Xamarin.Mac Internals , a także objaśnienie Register atrybutów i Export używanych do podłączania klas języka C# do Objective-C obiektów i elementów interfejsu użytkownika.

Bezpośredni dostęp SQLite

W przypadku danych SQLite, które mają być powiązane z elementami interfejsu użytkownika w narzędziu Interface Builder programu Xcode, zdecydowanie zaleca się bezpośredni dostęp do bazy danych SQLite (w przeciwieństwie do używania techniki, takiej jak ORM), ponieważ masz całkowitą kontrolę nad sposobem zapisywania i odczytywania danych z bazy danych.

Jak widzieliśmy w dokumentacji powiązania danych i kodowania klucz-wartość, korzystając z technik kodowania klucz-wartość i powiązania danych w aplikacji Xamarin.Mac, możesz znacznie zmniejszyć ilość kodu, który trzeba napisać i obsługiwać, aby wypełnić elementy interfejsu użytkownika i pracować z elementami interfejsu użytkownika. W połączeniu z bezpośrednim dostępem do bazy danych SQLite może również znacznie zmniejszyć ilość kodu wymaganego do odczytu i zapisu danych w tej bazie danych.

W tym artykule zmodyfikujemy przykładową aplikację z dokumentu powiązania danych i kodowania klucz-wartość, aby użyć bazy danych SQLite jako źródła zapasowego powiązania.

Obsługa bazy danych SQLite

Zanim będziemy mogli kontynuować, musimy dodać obsługę bazy danych SQLite do naszej aplikacji, dołączając odwołania do kilku elementów . Pliki DLL.

Należy wykonać następujące czynności:

  1. W okienku rozwiązania kliknij prawym przyciskiem myszy folder Odwołania i wybierz polecenie Edytuj odwołania.

  2. Wybierz zestawy Mono.Data.Sqlite i System.Data :

    Dodawanie wymaganych odwołań

  3. Kliknij przycisk OK, aby zapisać zmiany i dodać odwołania.

Modyfikowanie modelu danych

Teraz, gdy dodaliśmy obsługę bezpośredniego uzyskiwania dostępu do bazy danych SQLite do naszej aplikacji, musimy zmodyfikować obiekt modelu danych, aby odczytywać i zapisywać dane z bazy danych (a także dostarczać kodowanie klucz-wartość i powiązanie danych). W przypadku przykładowej aplikacji zmodyfikujemy klasę PersonModel.cs i sprawimy, że będzie ona wyglądać następująco:

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
    }
}

Przyjrzyjmy się modyfikacjom szczegółowo poniżej.

Najpierw dodaliśmy kilka instrukcji using, które są wymagane do używania biblioteki SQLite i dodaliśmy zmienną w celu zapisania ostatniego połączenia z bazą danych SQLite:

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

private SqliteConnection _conn = null;

Użyjemy tego zapisanego połączenia, aby automatycznie zapisać wszelkie zmiany rekordu w bazie danych, gdy użytkownik modyfikuje zawartość w interfejsie użytkownika za pomocą powiązania danych:

[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);
    }
}

Wszelkie zmiany wprowadzone we właściwościach Name, Occupation lub isManager zostaną wysłane do bazy danych, jeśli dane zostały tam zapisane wcześniej (np. jeśli zmienna _conn nie nulljest ). Następnie przyjrzyjmy się metodom, które dodaliśmy do tworzenia, aktualizowania, ładowania i usuwania osób z bazy danych.

Tworzenie nowego rekordu

Do utworzenia nowego rekordu w bazie danych SQLite dodano następujący kod:

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;
}

Używamy elementu , SQLiteCommand aby utworzyć nowy rekord w bazie danych. Otrzymujemy nowe polecenie z SQLiteConnection metody (conn), która została przekazana do metody przez wywołanie metody CreateCommand. Następnie ustawiliśmy instrukcję SQL tak, aby faktycznie zapisała nowy rekord, podając parametry dla rzeczywistych wartości:

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

Później ustawiliśmy wartości parametrów przy użyciu Parameters.AddWithValue metody w SQLiteCommandpliku . Korzystając z parametrów, upewniamy się, że wartości (takie jak pojedynczy cudzysłów) są prawidłowo zakodowane przed wysłaniem do biblioteki SQLite. Przykład:

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

Na koniec, ponieważ osoba może być menedżerem i mieć pod nimi kolekcję pracowników, rekursywnie wywołujemy Create metodę dla tych osób, aby zapisać je również w bazie danych:

// 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);
}

Aktualizowanie rekordu

Dodano następujący kod w celu zaktualizowania istniejącego rekordu w bazie danych SQLite:

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;
}

Podobnie jak powyżej utwórz , otrzymamy element SQLiteCommand z przekazanego elementu SQLiteConnectioni ustawimy nasz program SQL, aby zaktualizować nasz rekord (podając parametry):

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

Wypełniamy wartości parametrów (przykład: command.Parameters.AddWithValue ("@COL1", ID);) i ponownie cyklicznie wywołujemy aktualizację dla wszystkich rekordów podrzędnych:

// 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);
}

Ładowanie rekordu

Do załadowania istniejącego rekordu z bazy danych SQLite dodano następujący kod:

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;
}

Ponieważ procedury mogą być wywoływane rekursywnie z obiektu nadrzędnego (na przykład obiektu menedżera ładującego obiekt pracowników), dodano specjalny kod do obsługi otwierania i zamykania połączenia z bazą danych:

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 ();
}

Jak zawsze ustawiamy naszą usługę SQL, aby pobrać rekord i użyć parametrów:

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

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

Na koniec użyjemy czytnika danych, aby wykonać zapytanie i zwrócić pola rekordu PersonModel (które kopiujemy do wystąpienia klasy):

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];
    }
}

Jeśli ta osoba jest menedżerem, musimy również załadować wszystkich swoich pracowników (ponownie, rekursywnie wywołując ich Load metodę):

// 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);
            }
        }
    }
}

Usuwanie rekordu

Do usunięcia istniejącego rekordu z bazy danych SQLite dodano następujący kod:

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;
}

W tym miejscu udostępniamy sql, aby usunąć zarówno rekord menedżerów, jak i rekordy wszystkich pracowników w ramach tego menedżera (przy użyciu parametrów):

// 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);

Po usunięciu rekordu wyczyścimy bieżące wystąpienie PersonModel klasy:

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

Inicjowanie bazy danych

Po wprowadzeniu zmian w naszym modelu danych w celu obsługi odczytu i zapisu w bazie danych musimy otworzyć połączenie z bazą danych i zainicjować go w pierwszym uruchomieniu. Dodajmy następujący kod do naszego pliku MainWindow.cs :

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;
}

Przyjrzyjmy się bliżej powyższej kodzie. Najpierw wybieramy lokalizację nowej bazy danych (w tym przykładzie pulpit użytkownika), sprawdź, czy baza danych istnieje, a jeśli nie, utwórz ją:

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);

Następnie ustanowimy połączenie z bazą danych przy użyciu utworzonej powyżej ścieżki:

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

Następnie utworzymy wszystkie tabele SQL w bazie danych, których potrzebujemy:

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 ();

Na koniec użyjemy naszego modelu danych (PersonModel), aby utworzyć domyślny zestaw rekordów dla bazy danych przy pierwszym uruchomieniu aplikacji lub jeśli brakuje bazy danych:

// 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);

Po uruchomieniu aplikacji i otwarciu okna głównego utworzymy połączenie z bazą danych przy użyciu kodu dodanego powyżej:

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

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

Ładowanie powiązanych danych

Ze wszystkimi składnikami umożliwiającymi bezpośredni dostęp do powiązanych danych z bazy danych SQLite możemy załadować dane w różnych widokach, które udostępnia nasza aplikacja, i będą automatycznie wyświetlane w interfejsie użytkownika.

Ładowanie pojedynczego rekordu

Aby załadować pojedynczy rekord, w którym jest wiadomo identyfikator, możemy użyć następującego kodu:

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

Ładowanie wszystkich rekordów

Aby załadować wszystkie osoby, niezależnie od tego, czy są menedżerami, czy nie, użyj następującego kodu:

// 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 ();

W tym miejscu używamy przeciążenia konstruktora klasy PersonModel w celu załadowania osoby do pamięci:

var person = new PersonModel (_conn, childID);

Wywołujemy również klasę Data Bound, aby dodać osobę do naszej kolekcji osób AddPerson (person), co gwarantuje, że nasz interfejs użytkownika rozpoznaje zmianę i wyświetla ją:

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

Ładowanie tylko rekordów najwyższego poziomu

Aby załadować tylko menedżerów (na przykład w celu wyświetlenia danych w widoku konspektu), użyjemy następującego kodu:

// 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 ();

Jedyna rzeczywista różnica w instrukcji SQL (która ładuje tylko menedżerów command.CommandText = "SELECT ID FROM [People] WHERE isManager = 1"), ale działa tak samo jak w powyższej sekcji.

Bazy danych i pola kombi

Kontrolki menu dostępne dla systemu macOS (takie jak pole kombi) można ustawić tak, aby wypełnić listę rozwijaną z wewnętrznej listy (którą można wstępnie zdefiniować w narzędziu Interface Builder lub wypełnić za pomocą kodu) lub podając własne niestandardowe, zewnętrzne źródło danych. Aby uzyskać więcej informacji, zobacz Zapewnianie danych kontrolki menu.

Na przykład zmodyfikuj powyższy przykład prostego powiązania w narzędziu Interface Builder, dodaj pole kombi i uwidocznij je przy użyciu gniazda o nazwie EmployeeSelector:

Uwidacznianie gniazda pola kombi

W Inspektorze atrybutów sprawdź właściwości Autouzupełniania i Używa źródła danych:

Konfigurowanie atrybutów pola kombi

Zapisz zmiany i wróć do Visual Studio dla komputerów Mac, aby przeprowadzić synchronizację.

Dostarczanie danych pola kombi

Następnie dodaj nową klasę do projektu o nazwie ComboBoxDataSource i utwórz ją w następujący sposób:

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
    }
}

W tym przykładzie tworzysz nową, NSComboBoxDataSource która może przedstawiać elementy pola kombi z dowolnego źródła danych SQLite. Najpierw definiujemy następujące właściwości:

  • Conn — Pobiera lub ustawia połączenie z bazą danych SQLite.
  • TableName - Pobiera lub ustawia nazwę tabeli.
  • IDField — Pobiera lub ustawia pole, które zawiera unikatowy identyfikator dla danej tabeli. Domyślna wartość to ID.
  • DisplayField — Pobiera lub ustawia pole wyświetlane na liście rozwijanej.
  • RecordCount — Pobiera liczbę rekordów w danej tabeli.

Podczas tworzenia nowego wystąpienia obiektu przekazujemy połączenie, nazwę tabeli, opcjonalnie pole identyfikatora i pole wyświetlania:

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

Metoda GetRecordCount zwraca liczbę rekordów w podanej tabeli:

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;
}

Jest ona wywoływana TableNameza każdym razem, gdy wartość właściwości lub IDField DisplayField zostanie zmieniona.

Metoda IDForIndex zwraca unikatowy identyfikator (IDField) dla rekordu w danym indeksie elementu listy rozwijanej:

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;
}

Metoda ValueForIndex zwraca wartość (DisplayField) dla elementu w danym indeksie listy rozwijanej:

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;
}

Metoda IDForValue zwraca unikatowy identyfikator (IDField) dla danej wartości (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;
}

Funkcja ItemCount zwraca wstępnie skompilowaną liczbę elementów na liście obliczoną podczas TableNamezmiany właściwości lub IDField DisplayField :

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

Metoda ObjectValueForItem udostępnia wartość (DisplayField) dla danego indeksu elementu listy rozwijanej:

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;
}

Zwróć uwagę, że używamy instrukcji LIMIT i OFFSET w poleceniu SQLite, aby ograniczyć ten rekord, który jest potrzebny.

Metoda IndexOfItem zwraca indeks elementu listy rozwijanej wartości (DisplayField) podanej:

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;
}

Jeśli nie można odnaleźć wartości, zostanie zwrócona NSRange.NotFound wartość, a wszystkie elementy zostaną zaznaczone na liście rozwijanej.

Metoda CompletedString zwraca pierwszą zgodną wartość (DisplayField) dla częściowo wpisanego wpisu:

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;
}

Wyświetlanie danych i reagowanie na zdarzenia

Aby zebrać wszystkie elementy, zmodyfikuj element SubviewSimpleBindingController i ustaw go w następujący sposób:

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
    }
}

Właściwość DataSource udostępnia skrót do (utworzonego powyżej) dołączonego ComboBoxDataSource do pola kombi.

Metoda LoadSelectedPerson ładuje osobę z bazy danych dla danego unikatowego identyfikatora:

private void LoadSelectedPerson (string id)
{

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

W zastąpieniu AwakeFromNib metody najpierw dołączymy wystąpienie niestandardowego źródła danych pola kombi:

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

Następnie reagujemy na użytkownika edytując wartość tekstową pola kombi, wyszukując skojarzony unikatowy identyfikator (IDField) danych prezentowanych i ładując daną osobę, jeśli zostanie znaleziona:

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

Załadujemy również nową osobę, jeśli użytkownik wybierze nowy element z listy rozwijanej:

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

Na koniec automatycznie wypełniamy pole kombi i wyświetlamy osobę z pierwszym elementem na liście:

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

SQLite.NET ORM

Jak wspomniano powyżej, przy użyciu SQLite.NET menedżera relacji obiektów typu open source (ORM) możemy znacznie zmniejszyć ilość kodu wymaganego do odczytu i zapisu danych z bazy danych SQLite. Może to nie być najlepsza trasa do podjęcia w przypadku powiązania danych z powodu kilku wymagań, które kodowanie klucz-wartość i powiązanie danych umieszczają w obiekcie.

Według witryny internetowej SQLite.Net " SQLite to biblioteka oprogramowania, która implementuje samodzielny, bezserwerowy, zerowy, transakcyjny aparat bazy danych SQL. SQLite to najczęściej wdrażany aparat bazy danych na świecie. Kod źródłowy sqlite znajduje się w domenie publicznej.

W poniższych sekcjach pokażemy, jak używać SQLite.Net do udostępniania danych dla widoku tabeli.

Dołączanie SQLite.net NuGet

SQLite.NET jest przedstawiany jako pakiet NuGet uwzględniony w aplikacji. Aby można było dodać obsługę bazy danych przy użyciu SQLite.NET, musimy dołączyć ten pakiet.

Wykonaj następujące czynności, aby dodać pakiet:

  1. W okienku rozwiązania kliknij prawym przyciskiem myszy folder Packages i wybierz polecenie Dodaj pakiety...

  2. Wprowadź SQLite.net ciąg w polu wyszukiwania i wybierz wpis sqlite-net :

    Dodawanie pakietu NuGet SQLite

  3. Kliknij przycisk Dodaj pakiet, aby zakończyć.

Tworzenie modelu danych

Dodajmy nową klasę do projektu i wywołajmy metodę w pliku OccupationModel. Następnie zmodyfikujmy plik OccupationModel.cs i utwórzmy go w następujący sposób:

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
    }
}

Najpierw uwzględnimy SQLite.NET (using Sqlite), a następnie udostępnimy kilka właściwości, z których każda zostanie zapisana w bazie danych po zapisaniu tego rekordu. Pierwsza właściwość, która tworzymy jako klucz podstawowy i ustawia ją na automatyczne zwiększanie w następujący sposób:

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

Inicjowanie bazy danych

Po wprowadzeniu zmian w naszym modelu danych w celu obsługi odczytu i zapisu w bazie danych musimy otworzyć połączenie z bazą danych i zainicjować go w pierwszym uruchomieniu. Dodajmy następujący kod:

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;
}

Najpierw uzyskujemy ścieżkę do bazy danych (w tym przypadku pulpitu użytkownika) i sprawdź, czy baza danych już istnieje:

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);

Następnie ustanowimy połączenie z bazą danych w ścieżce utworzonej powyżej:

var conn = new SQLiteConnection (db);

Na koniec utworzymy tabelę i dodamy kilka rekordów domyślnych:

// 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);

Dodawanie widoku tabeli

Jako przykładowe użycie dodamy widok tabeli do naszego interfejsu użytkownika w konstruktorze interfejsu Xcode. Uwidocznimy ten widok tabeli za pośrednictwem gniazda (OccupationTable), abyśmy mogli uzyskać do niego dostęp za pośrednictwem kodu języka C#:

Uwidacznianie wylotu widoku tabeli

Następnie dodamy klasy niestandardowe, aby wypełnić tę tabelę danymi z bazy danych SQLite.NET.

Tworzenie źródła danych tabeli

Utwórzmy niestandardowe źródło danych, aby zapewnić dane dla naszej tabeli. Najpierw dodaj nową klasę o nazwie TableORMDatasource i utwórz ją następującą:

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
    }
}

Po utworzeniu wystąpienia tej klasy później przekażemy otwarte połączenie z bazą danych SQLite.NET. Metoda LoadOccupations wysyła zapytanie do bazy danych i kopiuje znalezione rekordy do pamięci (przy użyciu naszego OccupationModel modelu danych).

Tworzenie delegata tabeli

Końcowa klasa, którą potrzebujemy, to niestandardowy delegat tabeli, aby wyświetlić informacje załadowane z bazy danych SQLite.NET. Dodajmy nowy TableORMDelegate element do naszego projektu i utwórzmy go w następujący sposób:

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
    }
}

W tym miejscu użyjemy kolekcji źródła Occupations danych (załadowanej z bazy danych SQLite.NET), aby wypełnić kolumny tabeli za pomocą GetViewForItem metody zastąpienia.

Wypełnianie tabeli

Po wprowadzeniu wszystkich elementów wypełnijmy naszą tabelę, gdy zostanie ona zawyżona z pliku .xib, przesłaniając metodę AwakeFromNib i tworząc ją następującą:

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);
}

Najpierw uzyskamy dostęp do naszej bazy danych SQLite.NET, tworząc ją i wypełniając ją, jeśli jeszcze nie istnieje. Następnie utworzymy nowe wystąpienie niestandardowego źródła danych tabeli, przekażemy połączenie z bazą danych i dołączymy je do tabeli. Na koniec utworzymy nowe wystąpienie naszego niestandardowego delegata tabeli, przekażemy źródło danych i dołączymy je do tabeli.

Podsumowanie

W tym artykule szczegółowo przedstawiono pracę z powiązaniem danych i kodowaniem klucz-wartość przy użyciu baz danych SQLite w aplikacji Xamarin.Mac. Najpierw przyjrzeliśmy się uwidacznieniu klasy Objective-C C# przy użyciu kodowania klucz-wartość (KVC) i obserwowania wartości klucz-wartość (KVO). Następnie pokazano, jak używać klasy zgodnej z KVO i powiązać je z elementami interfejsu użytkownika w narzędziu Interface Builder programu Xcode. W artykule omówiono również pracę z danymi SQLite za pośrednictwem SQLite.NET ORM i wyświetlanie tych danych w widoku tabeli.