Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 基礎篇
- 2020 年 4 月 3 日
- 筆記
本著每天記錄一點成長一點的原則,打算將目前完成的一個WPF項目相關的技術分享出來,供團隊學習與總結。
總共分三個部分:
基礎篇主要爭對C#初學者,鞏固C#常用知識點;
中級篇主要爭對WPF布局與美化,在減輕程式碼量的情況做出漂亮的應用;
終極篇為框架應用實戰,包含MVVM框架Prism,ORM框架EntityFramework Core,開源資料庫Postgresql。
目錄
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 基礎篇
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 中級篇(待續)
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 終極篇(待續)
前言
此篇為C#常用知識點的實例說明,如果你是多年C#開發者可以跳過此篇或者只關注最後的新特性。
1、OOP之源 類與實例
一切事物皆對象。
類像產品模版,用它可以生產很多產品(簡稱實例對象)。
類:具有相同屬性和行為的對象的抽象集合。實例對象:具備一組可識別的特性與行為的實體。
舉個例子:張三、李四。
幻化出類如下:屬性為名字,實例就是張三、李四。
public class Person { public string Name { get; set; } public Person(string name) { Name = name; } } 張三=new Person("張三") 李四=new Person("李四")
類與屬性的修飾符中需要特別關注如下三個:
sealed:密封效果
- 修飾類時,類將不可以作為基類繼承。
- 修飾屬性與行為時,屬性與行為在繼承類中無法Override與New。

sealed class SealedClass { public int x; public int y; } // error class SealedTest2:SealedClass class SealedTest2 { static void Main() { var sc = new SealedClass(); sc.x = 110; sc.y = 150; Console.WriteLine($"x = {sc.x}, y = {sc.y}"); } } // Output: x = 110, y = 150 //------------------ class X { protected virtual void F() { Console.WriteLine("X.F"); } protected virtual void F2() { Console.WriteLine("X.F2"); } } class Y : X { sealed protected override void F() { Console.WriteLine("Y.F"); } protected override void F2() { Console.WriteLine("Y.F2"); } } class Z : Y { // Attempting to override F causes compiler error CS0239. // protected override void F() { Console.WriteLine("Z.F"); } // Overriding F2 is allowed. protected override void F2() { Console.WriteLine("Z.F2"); } }
View Code
internal:程式集訪問控制
- 只有在同一程式集的文件中,內部類型或成員才可訪問。
- 可以修飾類與成員,通常用於組件開發。

// Assembly1.cs // Compile with: /target:library internal class BaseClass { public static int intM = 0; } // Assembly1_a.cs // Compile with: /reference:Assembly1.dll class TestAccess { static void Main() { var myBase = new BaseClass(); // CS0122 錯誤 } }
View Code
protected:成員訪問控制
- 只能修飾成員,不可以修飾類。
- 修飾的成員,派生類內部可以直接訪問。

class A { protected int x = 123; } class B : A { static void Main() { var a = new A(); var b = new B(); // Error CS1540, because x can only be accessed by // classes derived from A. // a.x = 10; // OK, because this class derives from A. b.x = 10; } }
View Code
2、OOP之三大特性:
1、封裝
實例化的每個對象都包含它能進行操作所需要的全部資訊。
好處:減少耦合,在類結構變化時創建的對象可以跟隨變化;設置訪問限制,對外介面清晰明了。
2、繼承 (is-a關係)
站在巨人的肩膀上才能飛得更高。
通過繼承,除了被繼承類的特性之外,可以通過重寫、添加和修改創建自己的獨有特性。由於父子關係的原因導致類之間強耦合,最好在is-a關係時使用,has-a關係就不行了。
- New修飾:顯式指示成員不應作為基類成員的重寫。
- 抽象方法:abstract 修飾,必須在直接繼承自該類的任何非抽象類中重寫該方法。 如果派生類本身是抽象的,則它會繼承抽象成員而不會實現它們。
- 虛方法:virtual修飾,派生類可以根據需要重寫該方法,也可以直接使用基類的實現。
3、多態
程式碼使用基類方法,實際運行時調用派生類對象的重寫方法。
相當於派生類的對象可以作為基類的對象處理。 在出現此多形性時,該對象的聲明類型不再與運行時類型相同。

public class Shape { // A few example members public int X { get; private set; } public int Y { get; private set; } public int Height { get; set; } public int Width { get; set; } // Virtual method public virtual void Draw() { Console.WriteLine("Performing base class drawing tasks"); } } public class Circle : Shape { public override void Draw() { // Code to draw a circle... Console.WriteLine("Drawing a circle"); base.Draw(); } } public class Rectangle : Shape { public override void Draw() { // Code to draw a rectangle... Console.WriteLine("Drawing a rectangle"); base.Draw(); } } public class Triangle : Shape { public override void Draw() { // Code to draw a triangle... Console.WriteLine("Drawing a triangle"); base.Draw(); } } // Polymorphism at work #1: a Rectangle, Triangle and Circle // can all be used whereever a Shape is expected. No cast is // required because an implicit conversion exists from a derived // class to its base class. var shapes = new List<Shape> { new Rectangle(), new Triangle(), new Circle() }; // Polymorphism at work #2: the virtual method Draw is // invoked on each of the derived classes, not the base class. foreach (var shape in shapes) { shape.Draw(); } /* Output: Drawing a rectangle Performing base class drawing tasks Drawing a triangle Performing base class drawing tasks Drawing a circle Performing base class drawing tasks */
View Code
3、介面與抽象類
在設計的時候有時候很難決定用那種。原則有一個:如果不想波及子類的修改就用抽象類。因為介面定義之後,繼承的類必須實現此介面。
- 介面:包含方法、 屬性、 事件和索引器,成員隱式都具有公共訪問許可權,介面只定義不實現成員。一個介面可能從多個基介面繼承,類或結構可以實現多個介面。
- 抽象類:包含抽象方法,虛方法,特有行為,屬性,成員的訪問許可權可控制。sealed不可以使用,且只能繼承一個抽象類。
介面實例:

public delegate void StringListEvent(IStringList sender); public interface IStringList { void Add(string s); int Count { get; } event StringListEvent Changed; string this[int index] { get; set; } }
View Code
抽象類實例:

abstract class A { public abstract void F(); } abstract class B: A { public void G() {} } class C: B { public override void F() { // actual implementation of F } }
View Code
4、泛型
在客戶端程式碼聲明並初始化這些類或方法之前,這些類或方法會延遲指定一個或多個類型。不會產生運行時轉換或裝箱操作的成本或風險。
- 使用泛型類型可以最大限度地重用程式碼、保護類型安全性以及提高性能。
- 泛型最常見的用途是創建集合類。
- 可以創建自己的泛型介面、泛型類、泛型方法、泛型事件和泛型委託。
- 可以對泛型類進行約束以訪問特定數據類型的方法。

// Declare the generic class. public class GenericList<T> { public void Add(T input) { } } class TestGenericList { private class ExampleClass { } static void Main() { // Declare a list of type int. GenericList<int> list1 = new GenericList<int>(); list1.Add(1); // Declare a list of type string. GenericList<string> list2 = new GenericList<string>(); list2.Add(""); // Declare a list of type ExampleClass. GenericList<ExampleClass> list3 = new GenericList<ExampleClass>(); list3.Add(new ExampleClass()); } }
View Code
泛型約束:
class Base { } class Test<T, U> where U : struct where T : Base, new() { }
where T : notnull 指定類型參數必須是不可為 null 的值類型或不可為 null 的引用類型(C# 8.0)
where T : unmanaged 指定類型參數必須是不可為 null 的非託管類型(C# 7.3).通過 unmanaged
約束,用戶能編寫可重用常式,從而使用可作為記憶體塊操作的類型。

unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged { var size = sizeof(T); var result = new Byte[size]; Byte* p = (byte*)&argument; for (var i = 0; i < size; i++) result[i] = *p++; return result; }
View Code
5、擴展方法
擴展方法使你能夠向現有類型“添加”方法,而無需創建新的派生類型、重新編譯或以其他方式修改原始類型。 擴展方法是一種特殊的靜態方法,但可以像擴展類型上的實例方法一樣進行調用。Dapper,System.Linq(對System.Collections.IEnumerable 和 System.Collections.Generic.IEnumerable<T> 的擴展)就是經典使用。
擴展方法的優先順序總是比類型本身中定義的實例方法低,且只能訪問公有成員。 只在不得已的情況下才實現擴展方法。
如果確實為給定類型實現了擴展方法,請記住以下幾點:
-
如果擴展方法與該類型中定義的方法具有相同的簽名,則擴展方法永遠不會被調用。
-
在命名空間級別將擴展方法置於範圍中。 例如,如果你在一個名為
Extensions
的命名空間中具有多個包含擴展方法的靜態類,則這些擴展方法將全部由using Extensions;
指令置於範圍中。

namespace ExtensionMethods { public static class MyExtensions { public static int WordCount(this String str) { return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length; } } }
View Code
6、語言集成查詢(LINQ)
一系列直接將查詢功能集成到 C# 語言的技術統稱。 傳統上,針對數據的查詢都以簡單的字元串表示,而沒有編譯時類型檢查或 IntelliSense 支援。 此外,還需要針對每種數據源學習一種不同的查詢語言:SQL 資料庫、XML 文檔、各種 Web 服務等等。 藉助 LINQ,查詢成為了最高級的語言構造,就像類、方法和事件一樣。
簡單點就是使用查詢表達式可以查詢和轉換 SQL 資料庫、ADO .NET 數據集、XML 文檔和流以及 .NET 集合中的數據。超越了限定於集合的擴展方法System.Linq。Entity Framework就是基於這個自動生成查詢Sql操作數據。
-
查詢表達式中的變數全都是強類型,儘管在許多情況下,無需顯式提供類型,因為編譯器可以推斷出。 有關詳細資訊,請參閱 LINQ 查詢操作中的類型關係。
-
只有在循環訪問查詢變數後,才會執行查詢(例如,在
foreach
語句中)。 有關詳細資訊,請參閱 LINQ 查詢簡介。 - 應用程式始終將源數據視為 IEnumerable<T> 或 IQueryable<T> 集合。
查詢表達式:
查詢表達式必須以 from 子句開頭,且必須以 select 或 group 子句結尾。 在第一個 from
子句與最後一個 select
或 group
子句之間,可以包含以下這些可選子句中的一個或多個:where、orderby、join、let,甚至是其他 from 子句。 還可以使用 into 關鍵字,使 join
或 group
子句的結果可以充當相同查詢表達式中的其他查詢子句的源。

// percentileQuery is an IEnumerable<IGrouping<int, Country>> var percentileQuery = from country in countries let percentile = (int) country.Population / 10_000_000 group country by percentile into countryGroup where countryGroup.Key >= 20 orderby countryGroup.Key select countryGroup; // grouping is an IGrouping<int, Country> foreach (var grouping in percentileQuery) { Console.WriteLine(grouping.Key); foreach (var country in grouping) Console.WriteLine(country.Name + ":" + country.Population); }
View Code
參見內聯實例:

class Person { public string FirstName { get; set; } public string LastName { get; set; } } class Pet { public string Name { get; set; } public Person Owner { get; set; } } class Cat : Pet { } class Dog : Pet { } public static void MultipleJoinExample() { Person magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" }; Person terry = new Person { FirstName = "Terry", LastName = "Adams" }; Person charlotte = new Person { FirstName = "Charlotte", LastName = "Weiss" }; Person arlene = new Person { FirstName = "Arlene", LastName = "Huff" }; Person rui = new Person { FirstName = "Rui", LastName = "Raposo" }; Person phyllis = new Person { FirstName = "Phyllis", LastName = "Harris" }; Cat barley = new Cat { Name = "Barley", Owner = terry }; Cat boots = new Cat { Name = "Boots", Owner = terry }; Cat whiskers = new Cat { Name = "Whiskers", Owner = charlotte }; Cat bluemoon = new Cat { Name = "Blue Moon", Owner = rui }; Cat daisy = new Cat { Name = "Daisy", Owner = magnus }; Dog fourwheeldrive = new Dog { Name = "Four Wheel Drive", Owner = phyllis }; Dog duke = new Dog { Name = "Duke", Owner = magnus }; Dog denim = new Dog { Name = "Denim", Owner = terry }; Dog wiley = new Dog { Name = "Wiley", Owner = charlotte }; Dog snoopy = new Dog { Name = "Snoopy", Owner = rui }; Dog snickers = new Dog { Name = "Snickers", Owner = arlene }; // Create three lists. List<Person> people = new List<Person> { magnus, terry, charlotte, arlene, rui, phyllis }; List<Cat> cats = new List<Cat> { barley, boots, whiskers, bluemoon, daisy }; List<Dog> dogs = new List<Dog> { fourwheeldrive, duke, denim, wiley, snoopy, snickers }; // The first join matches Person and Cat.Owner from the list of people and // cats, based on a common Person. The second join matches dogs whose names start // with the same letter as the cats that have the same owner. var query = from person in people join cat in cats on person equals cat.Owner join dog in dogs on new { Owner = person, Letter = cat.Name.Substring(0, 1) } equals new { dog.Owner, Letter = dog.Name.Substring(0, 1) } select new { CatName = cat.Name, DogName = dog.Name }; foreach (var obj in query) { Console.WriteLine( $"The cat "{obj.CatName}" shares a house, and the first letter of their name, with "{obj.DogName}"."); } } // This code produces the following output: // // The cat "Daisy" shares a house, and the first letter of their name, with "Duke". // The cat "Whiskers" shares a house, and the first letter of their name, with "Wiley".
View Code
詳細參照:https://docs.microsoft.com/zh-cn/dotnet/csharp/linq/
7、lambda表達式
匿名函數演變而來。匿名函數是一個“內聯”語句或表達式,可在需要委託類型的任何地方使用。 可以使用匿名函數來初始化命名委託,或傳遞命名委託(而不是命名委託類型)作為方法參數。
在 C# 1.0 中,通過使用在程式碼中其他位置定義的方法顯式初始化委託來創建委託的實例。 C# 2.0 引入了匿名方法的概念,作為一種編寫可在委託調用中執行的未命名內聯語句塊的方式。 C# 3.0 引入了 lambda 表達式,這種表達式與匿名方法的概念類似,但更具表現力並且更簡練。任何 Lambda 表達式都可以轉換為委託類型。 Lambda 表達式可以轉換的委託類型由其參數和返回值的類型定義。 如果 lambda 表達式不返回值,則可以將其轉換為 Action
委託類型之一;否則,可將其轉換為 Func
委託類型之一。

class Test { delegate void TestDelegate(string s); static void M(string s) { Console.WriteLine(s); } static void Main(string[] args) { // Original delegate syntax required // initialization with a named method. TestDelegate testDelA = new TestDelegate(M); // C# 2.0: A delegate can be initialized with // inline code, called an "anonymous method." This // method takes a string as an input parameter. TestDelegate testDelB = delegate(string s) { Console.WriteLine(s); }; // C# 3.0. A delegate can be initialized with // a lambda expression. The lambda also takes a string // as an input parameter (x). The type of x is inferred by the compiler. TestDelegate testDelC = (x) => { Console.WriteLine(x); }; // Invoke the delegates. testDelA("Hello. My name is M and I write lines."); testDelB("That's nothing. I'm anonymous and "); testDelC("I'm a famous author."); // Keep console window open in debug mode. Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } /* Output: Hello. My name is M and I write lines. That's nothing. I'm anonymous and I'm a famous author. Press any key to exit. */
View Code

Action<string> greet = name => { string greeting = $"Hello {name}!"; Console.WriteLine(greeting); }; greet("World"); // Output: // Hello World!
View Code

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index); Console.WriteLine(string.Join(" ", firstSmallNumbers)); // Output: // 5 4
View Code
8、元組
使用之前必須添加 NuGet 包 System.ValueTuple,
C# 7.0添加的新功能。
元組主要作為返回多個值方法的返回值,簡化定義返回值類型的麻煩。與類和結構一樣,使用數據結構存儲多個元素,但不定義行為。 既可以獲得靜態類型檢查的所有好處,又不需要使用更複雜的 class
或 struct
語法來創作類型。元組還是對 private
或 internal
這樣的實用方法最有用。不過公共方法返回具有多個元素的值時,請創建用戶定義的類型(class
或 struct
類型)。

public static double StandardDeviation(IEnumerable<double> sequence) { var computation = ComputeSumAndSumOfSquares(sequence); var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count; return Math.Sqrt(variance / computation.Count); } private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable<double> sequence) { double sum = 0; double sumOfSquares = 0; int count = 0; foreach (var item in sequence) { count++; sum += item; sumOfSquares += item * item; } return (count, sum, sumOfSquares); }
View Code
元組析構:

public static void Main() { (string city, int population, double area) = QueryCityData("New York City"); // Do something with the data. } public static void Main() { var (city, population, area) = QueryCityData("New York City"); // Do something with the data. } public static void Main() { (string city, var population, var area) = QueryCityData("New York City"); // Do something with the data. } public static void Main() { string city = "Raleigh"; int population = 458880; double area = 144.8; (city, population, area) = QueryCityData("New York City"); // Do something with the data. }
View Code
棄元析構:

using System; using System.Collections.Generic; public class Example { public static void Main() { var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010); Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}"); } private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2) { int population1 = 0, population2 = 0; double area = 0; if (name == "New York City") { area = 468.48; if (year1 == 1960) { population1 = 7781984; } if (year2 == 2010) { population2 = 8175133; } return (name, area, year1, population1, year2, population2); } return ("", 0, 0, 0, 0, 0); } } // The example displays the following output: // Population change, 1960 to 2010: 393,149
View Code
類型的Deconstruct用戶自定義析構:

using System; public class Person { public string FirstName { get; set; } public string MiddleName { get; set; } public string LastName { get; set; } public string City { get; set; } public string State { get; set; } public Person(string fname, string mname, string lname, string cityName, string stateName) { FirstName = fname; MiddleName = mname; LastName = lname; City = cityName; State = stateName; } // Return the first and last name. public void Deconstruct(out string fname, out string lname) { fname = FirstName; lname = LastName; } public void Deconstruct(out string fname, out string mname, out string lname) { fname = FirstName; mname = MiddleName; lname = LastName; } public void Deconstruct(out string fname, out string lname, out string city, out string state) { fname = FirstName; lname = LastName; city = City; state = State; } } public class Example { public static void Main() { var p = new Person("John", "Quincy", "Adams", "Boston", "MA"); // Deconstruct the person object. var (fName, lName, city, state) = p; Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!"); } } // The example displays the following output: // Hello John Adams of Boston, MA!
View Code