diff --git a/samples/DataGridSample/App.xaml b/samples/DataGridSample/App.xaml new file mode 100644 index 0000000000..748fbd7dbf --- /dev/null +++ b/samples/DataGridSample/App.xaml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/samples/DataGridSample/App.xaml.cs b/samples/DataGridSample/App.xaml.cs new file mode 100644 index 0000000000..a0a01e2132 --- /dev/null +++ b/samples/DataGridSample/App.xaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace DataGridSample +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/DataGridSample/Countries.cs b/samples/DataGridSample/Countries.cs new file mode 100644 index 0000000000..fc1b3ac5b6 --- /dev/null +++ b/samples/DataGridSample/Countries.cs @@ -0,0 +1,533 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace DataGridSample +{ + public class Country + { + public string Name { get; private set; } + public string Region { get; private set; } + public int Population { get; private set; } + //Square Miles + public int Area { get; private set; } + //Per Square Mile + public double PopulationDensity { get; private set; } + //Coast / Area + public double CoastLine { get; private set; } + public double? NetMigration { get; private set; } + //per 1000 births + public double? InfantMortality { get; private set; } + public int GDP { get; private set; } + public double? LiteracyPercent { get; private set; } + //per 1000 + public double? Phones { get; private set; } + public double? BirthRate { get; private set; } + public double? DeathRate { get; private set; } + + public Country(string name, string region, int population, int area, double density, double coast, double? migration, + double? infantMorality, int gdp, double? literacy, double? phones, double? birth, double? death) + { + Name = name; + Region = region; + Population = population; + Area = area; + PopulationDensity = density; + CoastLine = coast; + NetMigration = migration; + InfantMortality = infantMorality; + GDP = gdp; + LiteracyPercent = literacy; + BirthRate = birth; + DeathRate = death; + } + } + + public static class Countries + { + static IEnumerable GetCountries() + { + yield return new Country("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 48, 0, 23.06, 163.07, 700, 36, 3.2, 46.6, 20.34); + yield return new Country("Albania", "EASTERN EUROPE", 3581655, 28748, 124.6, 1.26, -4.93, 21.52, 4500, 86.5, 71.2, 15.11, 5.22); + yield return new Country("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 13.8, 0.04, -0.39, 31, 6000, 70, 78.1, 17.14, 4.61); + yield return new Country("American Samoa", "OCEANIA", 57794, 199, 290.4, 58.29, -20.71, 9.27, 8000, 97, 259.5, 22.46, 3.27); + yield return new Country("Andorra", "WESTERN EUROPE", 71201, 468, 152.1, 0, 6.6, 4.05, 19000, 100, 497.2, 8.71, 6.25); + yield return new Country("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 9.7, 0.13, 0, 191.19, 1900, 42, 7.8, 45.11, 24.2); + yield return new Country("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 132.1, 59.8, 10.76, 21.03, 8600, 95, 460, 14.17, 5.34); + yield return new Country("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 156, 34.54, -6.15, 19.46, 11000, 89, 549.9, 16.93, 5.37); + yield return new Country("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 14.4, 0.18, 0.61, 15.18, 11200, 97.1, 220.4, 16.73, 7.55); + yield return new Country("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 99.9, 0, -6.47, 23.28, 3500, 98.6, 195.7, 12.07, 8.23); + yield return new Country("Aruba", "LATIN AMER. & CARIB", 71891, 193, 372.5, 35.49, 0, 5.89, 28000, 97, 516.1, 11.03, 6.68); + yield return new Country("Australia", "OCEANIA", 20264082, 7686850, 2.6, 0.34, 3.98, 4.69, 29000, 100, 565.5, 12.14, 7.51); + yield return new Country("Austria", "WESTERN EUROPE", 8192880, 83870, 97.7, 0, 2, 4.66, 30000, 98, 452.2, 8.74, 9.76); + yield return new Country("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 91.9, 0, -4.9, 81.74, 3400, 97, 137.1, 20.74, 9.75); + yield return new Country("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 21.8, 25.41, -2.2, 25.21, 16700, 95.6, 460.6, 17.57, 9.05); + yield return new Country("Bahrain", "NEAR EAST", 698585, 665, 1050.5, 24.21, 1.05, 17.27, 16900, 89.1, 281.3, 17.8, 4.14); + yield return new Country("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1023.4, 0.4, -0.71, 62.6, 1900, 43.1, 7.3, 29.8, 8.27); + yield return new Country("Barbados", "LATIN AMER. & CARIB", 279912, 431, 649.5, 22.51, -0.31, 12.5, 15700, 97.4, 481.9, 12.71, 8.67); + yield return new Country("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 49.6, 0, 2.54, 13.37, 6100, 99.6, 319.1, 11.16, 14.02); + yield return new Country("Belgium", "WESTERN EUROPE", 10379067, 30528, 340, 0.22, 1.23, 4.68, 29100, 98, 462.6, 10.38, 10.27); + yield return new Country("Belize", "LATIN AMER. & CARIB", 287730, 22966, 12.5, 1.68, 0, 25.69, 4900, 94.1, 115.7, 28.84, 5.72); + yield return new Country("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 69.8, 0.11, 0, 85, 1100, 40.9, 9.7, 38.85, 12.22); + yield return new Country("Bermuda", "NORTHERN AMERICA", 65773, 53, 1241, 194.34, 2.49, 8.53, 36000, 98, 851.4, 11.4, 7.74); + yield return new Country("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 48.5, 0, 0, 100.44, 1300, 42.2, 14.3, 33.65, 12.7); + yield return new Country("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 8.2, 0, -1.32, 53.11, 2400, 87.2, 71.9, 23.3, 7.53); + yield return new Country("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 88, 0.04, 0.31, 21.05, 6100,null, 215.4, 8.77, 8.27); + yield return new Country("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 2.7, 0, 0, 54.58, 9000, 79.8, 80.5, 23.08, 29.5); + yield return new Country("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 22.1, 0.09, -0.03, 29.61, 7600, 86.4, 225.3, 16.56, 6.17); + yield return new Country("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 151, 52.29, 10.01, 18.05, 16000, 97.8, 506.5, 14.89, 4.42); + yield return new Country("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 65.8, 2.79, 3.59, 12.61, 18600, 93.9, 237.2, 18.79, 3.45); + yield return new Country("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 66.6, 0.32, -4.58, 20.55, 7600, 98.6, 336.3, 9.65, 14.27); + yield return new Country("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 50.7, 0, 0, 97.57, 1100, 26.6, 7, 45.62, 15.6); + yield return new Country("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 69.8, 0.28, -1.8, 67.24, 1800, 85.3, 10.1, 17.91, 9.83); + yield return new Country("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 290.7, 0, -0.06, 69.29, 600, 51.6, 3.4, 42.22, 13.46); + yield return new Country("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 76.7, 0.24, 0, 71.48, 1900, 69.4, 2.6, 26.9, 9.06); + yield return new Country("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 36.5, 0.08, 0, 68.26, 1800, 79, 5.7, 33.89, 13.47); + yield return new Country("Canada", "NORTHERN AMERICA", 33098932, 9984670, 3.3, 2.02, 5.96, 4.75, 29800, 97, 552.2, 10.78, 7.8); + yield return new Country("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 104.4, 23.93, -12.07, 47.77, 1400, 76.6, 169.6, 24.87, 6.55); + yield return new Country("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 173.4, 61.07, 18.75, 8.19, 35000, 98, 836.3, 12.74, 4.89); + yield return new Country("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 6.9, 0, 0, 91, 1100, 51, 2.3, 33.91, 18.65); + yield return new Country("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 7.7, 0, -0.11, 93.82, 1200, 47.5, 1.3, 45.73, 16.38); + yield return new Country("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 21.3, 0.85, 0, 8.8, 9900, 96.2, 213, 15.23, 5.81); + yield return new Country("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 136.9, 0.15, -0.4, 24.18, 5000, 90.9, 266.7, 13.25, 6.97); + yield return new Country("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 38.3, 0.28, -0.31, 20.97, 6300, 92.5, 176.2, 20.48, 5.58); + yield return new Country("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 318.4, 15.67, 0, 74.93, 700, 56.5, 24.5, 36.93, 8.2); + yield return new Country("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 26.7, 0, 0, 94.69, 700, 65.5, 0.2, 43.69, 13.27); + yield return new Country("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 10.8, 0.05, -0.17, 93.86, 700, 83.8, 3.7, 42.57, 12.93); + yield return new Country("Cook Islands", "OCEANIA", 21388, 240, 89.1, 50,null,null, 5000, 95, 289.9, 21,null); + yield return new Country("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 79.8, 2.52, 0.51, 9.95, 9100, 96, 340.7, 18.32, 4.36); + yield return new Country("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 54.8, 0.16, -0.07, 90.83, 1400, 50.9, 14.6, 35.11, 14.84); + yield return new Country("Croatia", "EASTERN EUROPE", 4494749, 56542, 79.5, 10.32, 1.58, 6.84, 10600, 98.5, 420.4, 9.61, 11.48); + yield return new Country("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 102.7, 3.37, -1.58, 6.33, 2900, 97, 74.7, 11.89, 7.22); + yield return new Country("Cyprus", "NEAR EAST", 784301, 9250, 84.8, 7.01, 0.43, 7.18, 19200, 97.6,null, 12.56, 7.68); + yield return new Country("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 129.8, 0, 0.97, 3.93, 15700, 99.9, 314.3, 9.02, 10.59); + yield return new Country("Denmark", "WESTERN EUROPE", 5450661, 43094, 126.5, 16.97, 2.48, 4.56, 31100, 100, 614.6, 11.13, 10.36); + yield return new Country("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 21.2, 1.37, 0, 104.13, 1300, 67.9, 22.8, 39.53, 19.31); + yield return new Country("Dominica", "LATIN AMER. & CARIB", 68910, 754, 91.4, 19.63, -13.87, 14.15, 5400, 94, 304.8, 15.27, 6.73); + yield return new Country("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 188.5, 2.64, -3.22, 32.38, 6000, 84.7, 97.4, 23.22, 5.73); + yield return new Country("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 70.8, 4.7, 0, 47.41, 500, 58.6,null, 26.99, 6.24); + yield return new Country("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 47.8, 0.79, -8.58, 23.66, 3300, 92.5, 125.6, 22.29, 4.23); + yield return new Country("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 78.8, 0.24, -0.22, 32.59, 4000, 57.7, 131.8, 22.94, 5.23); + yield return new Country("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 324.3, 1.46, -3.74, 25.1, 4800, 80.2, 142.4, 26.61, 5.78); + yield return new Country("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 19.3, 1.06, 0, 85.13, 2700, 85.7, 18.5, 35.59, 15.06); + yield return new Country("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 39.5, 1.84, 0, 74.87, 700, 58.6, 7.9, 34.33, 9.6); + yield return new Country("Estonia", "BALTICS", 1324333, 45226, 29.3, 8.39, -3.16, 7.87, 12300, 99.8, 333.8, 10.04, 13.25); + yield return new Country("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 66.3, 0, 0, 95.32, 700, 42.7, 8.2, 37.98, 14.86); + yield return new Country("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 33.8, 79.84, 1.41, 6.24, 22000,null, 503.8, 14.05, 8.7); + yield return new Country("Fiji", "OCEANIA", 905949, 18270, 49.6, 6.18, -3.14, 12.62, 5800, 93.7, 112.6, 22.55, 5.65); + yield return new Country("Finland", "WESTERN EUROPE", 5231372, 338145, 15.5, 0.37, 0.95, 3.57, 27400, 100, 405.3, 10.45, 9.86); + yield return new Country("France", "WESTERN EUROPE", 60876136, 547030, 111.3, 0.63, 0.66, 4.26, 27600, 99, 586.4, 11.99, 9.14); + yield return new Country("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 2.2, 0.42, 6.27, 12.07, 8300, 83, 255.6, 20.46, 4.88); + yield return new Country("French Polynesia", "OCEANIA", 274578, 4167, 65.9, 60.6, 2.94, 8.44, 17500, 98, 194.5, 16.68, 4.69); + yield return new Country("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5.3, 0.33, 0, 53.64, 5500, 63.2, 27.4, 36.16, 12.25); + yield return new Country("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 145.3, 0.71, 1.57, 72.02, 1700, 40.1, 26.8, 39.37, 12.25); + yield return new Country("Gaza Strip", "NEAR EAST", 1428757, 360, 3968.8, 11.11, 1.6, 22.93, 600,null, 244.3, 39.45, 3.8); + yield return new Country("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 66.9, 0.44, -4.7, 18.59, 2500, 99, 146.6, 10.41, 9.23); + yield return new Country("Germany", "WESTERN EUROPE", 82422299, 357021, 230.9, 0.67, 2.18, 4.16, 27600, 99, 667.9, 8.25, 10.62); + yield return new Country("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 93.6, 0.23, -0.64, 51.43, 2200, 74.8, 14.4, 30.52, 9.72); + yield return new Country("Gibraltar", "WESTERN EUROPE", 27928, 7, 3989.7, 171.43, 0, 5.13, 17500,null, 877.7, 10.74, 9.31); + yield return new Country("Greece", "WESTERN EUROPE", 10688058, 131940, 81, 10.37, 2.35, 5.53, 20000, 97.5, 589.7, 9.68, 10.24); + yield return new Country("Greenland", "NORTHERN AMERICA", 56361, 2166086, 0, 2.04, -8.37, 15.82, 20000,null, 448.9, 15.93, 7.84); + yield return new Country("Grenada", "LATIN AMER. & CARIB", 89703, 344, 260.8, 35.17, -13.92, 14.62, 5000, 98, 364.5, 22.08, 6.88); + yield return new Country("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 254.4, 17.19, -0.15, 8.6, 8000, 90, 463.8, 15.05, 6.09); + yield return new Country("Guam", "OCEANIA", 171019, 541, 316.1, 23.2, 0, 6.94, 21000, 99, 492, 18.79, 4.48); + yield return new Country("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 112.9, 0.37, -1.67, 35.93, 4100, 70.6, 92.1, 29.88, 5.2); + yield return new Country("Guernsey", "WESTERN EUROPE", 65409, 78, 838.6, 64.1, 3.84, 4.71, 20000,null, 842.4, 8.81, 10.01); + yield return new Country("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 39.4, 0.13, -3.06, 90.37, 2100, 35.9, 2.7, 41.76, 15.48); + yield return new Country("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 39.9, 0.97, -1.57, 107.17, 800, 42.4, 7.4, 37.22, 16.53); + yield return new Country("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 3.6, 0.21, -2.07, 33.26, 4000, 98.8, 143.5, 18.28, 8.28); + yield return new Country("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 299.4, 6.38, -3.4, 73.45, 1600, 52.9, 16.9, 36.44, 12.17); + yield return new Country("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 65.4, 0.73, -1.99, 29.32, 2600, 76.2, 67.5, 28.24, 5.28); + yield return new Country("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 6355.7, 67.12, 5.24, 2.97, 28800, 93.5, 546.7, 7.29, 6.29); + yield return new Country("Hungary", "EASTERN EUROPE", 9981334, 93030, 107.3, 0, 0.86, 8.57, 13900, 99.4, 336.2, 9.72, 13.11); + yield return new Country("Iceland", "WESTERN EUROPE", 299388, 103000, 2.9, 4.83, 2.38, 3.31, 30900, 99.9, 647.7, 13.64, 6.72); + yield return new Country("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 333.2, 0.21, -0.07, 56.29, 2900, 59.5, 45.4, 22.01, 8.18); + yield return new Country("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 127.9, 2.85, 0, 35.6, 3200, 87.9, 52, 20.34, 6.25); + yield return new Country("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 41.7, 0.15, -0.84, 41.58, 7000, 79.4, 276.4, 17, 5.55); + yield return new Country("Iraq", "NEAR EAST", 26783383, 437072, 61.3, 0.01, 0, 50.25, 1500, 40.4, 38.6, 31.98, 5.37); + yield return new Country("Ireland", "WESTERN EUROPE", 4062235, 70280, 57.8, 2.06, 4.99, 5.39, 29600, 98, 500.5, 14.45, 7.82); + yield return new Country("Isle of Man", "WESTERN EUROPE", 75441, 572, 131.9, 27.97, 5.36, 5.93, 21000,null, 676, 11.05, 11.19); + yield return new Country("Israel", "NEAR EAST", 6352117, 20770, 305.8, 1.31, 0.68, 7.03, 19800, 95.4, 462.3, 17.97, 6.18); + yield return new Country("Italy", "WESTERN EUROPE", 58133509, 301230, 193, 2.52, 2.07, 5.94, 26700, 98.6, 430.9, 8.72, 10.4); + yield return new Country("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 250.9, 9.3, -4.92, 12.36, 3900, 87.9, 124, 20.82, 6.52); + yield return new Country("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 337.4, 7.87, 0, 3.26, 28200, 99, 461.2, 9.37, 9.16); + yield return new Country("Jersey", "WESTERN EUROPE", 91084, 116, 785.2, 60.34, 2.76, 5.24, 24800,null, 811.3, 9.3, 9.28); + yield return new Country("Jordan", "NEAR EAST", 5906760, 92300, 64, 0.03, 6.59, 17.35, 4300, 91.3, 104.5, 21.25, 2.65); + yield return new Country("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 5.6, 0, -3.35, 29.21, 6300, 98.4, 164.1, 16, 9.42); + yield return new Country("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 59.6, 0.09, -0.1, 61.47, 1000, 85.1, 8.1, 39.72, 14.02); + yield return new Country("Kiribati", "OCEANIA", 105432, 811, 130, 140.94, 0, 48.52, 800,null, 42.7, 30.65, 8.26); + yield return new Country("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 191.8, 2.07, 0, 24.04, 1300, 99, 42.4, 15.54, 7.13); + yield return new Country("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 496, 2.45, 0, 7.05, 17800, 97.9, 486.1, 10, 5.85); + yield return new Country("Kuwait", "NEAR EAST", 2418393, 17820, 135.7, 2.8, 14.18, 9.95, 19000, 83.5, 211, 21.94, 2.41); + yield return new Country("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 26.3, 0, -2.45, 35.64, 1600, 97, 84, 22.8, 7.08); + yield return new Country("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 26.9, 0, 0, 85.22, 1700, 66.4, 14.1, 35.49, 11.55); + yield return new Country("Latvia", "BALTICS", 2274735, 64589, 35.2, 0.82, -2.23, 9.55, 10200, 99.8, 321.4, 9.24, 13.66); + yield return new Country("Lebanon", "NEAR EAST", 3874050, 10400, 372.5, 2.16, 0, 24.52, 4800, 87.4, 255.6, 18.52, 6.21); + yield return new Country("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 66.6, 0, -0.74, 84.23, 3000, 84.8, 23.7, 24.75, 28.71); + yield return new Country("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 27.3, 0.52, 0, 128.87, 1000, 57.5, 2.3, 44.77, 23.1); + yield return new Country("Libya", "NORTHERN AFRICA", 5900754, 1759540, 3.4, 0.1, 0, 24.6, 6400, 82.6, 127.1, 26.49, 3.48); + yield return new Country("Liechtenstein", "WESTERN EUROPE", 33987, 160, 212.4, 0, 4.85, 4.7, 25000, 100, 585.5, 10.21, 7.18); + yield return new Country("Lithuania", "BALTICS", 3585906, 65200, 55, 0.14, -0.71, 6.89, 11400, 99.6, 223.4, 8.75, 10.98); + yield return new Country("Luxembourg", "WESTERN EUROPE", 474413, 2586, 183.5, 0, 8.97, 4.81, 55100, 100, 515.4, 11.94, 8.41); + yield return new Country("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 16183, 146.43, 4.86, 4.39, 19400, 94.5, 384.9, 8.48, 4.47); + yield return new Country("Macedonia", "EASTERN EUROPE", 2050554, 25333, 80.9, 0, -1.45, 10.09, 6700,null, 260, 12.02, 8.77); + yield return new Country("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 31.7, 0.82, 0, 76.83, 800, 68.9, 3.6, 41.41, 11.11); + yield return new Country("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 109.8, 0, 0, 103.32, 600, 62.7, 7.9, 43.13, 19.33); + yield return new Country("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 74, 1.42, 0, 17.7, 9000, 88.7, 179, 22.86, 5.05); + yield return new Country("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 1196.7, 214.67, 0, 56.52, 3900, 97.2, 90, 34.81, 7.06); + yield return new Country("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 9.5, 0, -0.33, 116.79, 900, 46.4, 6.4, 49.82, 16.89); + yield return new Country("Malta", "WESTERN EUROPE", 400214, 316, 1266.5, 62.28, 2.07, 3.89, 17700, 92.8, 505, 10.22, 8.1); + yield return new Country("Marshall Islands", "OCEANIA", 60422, 11854, 5.1, 3.12, -6.04, 29.45, 1600, 93.7, 91.2, 33.05, 4.78); + yield return new Country("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 396.5, 31.82, -0.05, 7.09, 14400, 97.7, 394.4, 13.74, 6.48); + yield return new Country("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 3.1, 0.07, 0, 70.89, 1800, 41.7, 12.9, 40.99, 12.16); + yield return new Country("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 608.3, 8.68, -0.9, 15.03, 11400, 85.6, 289.3, 15.43, 6.86); + yield return new Country("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 538.1, 49.52, 6.78, 62.4, 2600,null, 49.7, 40.95, 7.7); + yield return new Country("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 54.5, 0.47, -4.87, 20.91, 9000, 92.2, 181.6, 20.69, 4.74); + yield return new Country("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 153.9, 870.66, -20.99, 30.21, 2000, 89, 114.8, 24.68, 4.75); + yield return new Country("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 132, 0, -0.26, 40.42, 1800, 99.1, 208.1, 15.7, 12.64); + yield return new Country("Monaco", "WESTERN EUROPE", 32543, 2, 16271.5, 205, 7.75, 5.43, 27000, 99, 1035.6, 9.19, 12.91); + yield return new Country("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1.8, 0, 0, 53.79, 1800, 97.8, 55.1, 21.59, 6.95); + yield return new Country("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 92.5, 39.22, 0, 7.35, 3400, 97,null, 17.59, 7.1); + yield return new Country("Morocco", "NORTHERN AFRICA", 33241259, 446550, 74.4, 0.41, -0.98, 41.62, 4000, 51.7, 40.4, 21.98, 5.58); + yield return new Country("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 24.6, 0.31, 0, 130.79, 1200, 47.8, 3.5, 35.18, 21.35); + yield return new Country("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 2.5, 0.19, 0, 48.98, 7200, 84, 62.6, 24.32, 18.86); + yield return new Country("Nauru", "OCEANIA", 13287, 21, 632.7, 142.86, 0, 9.95, 5000,null, 143, 24.76, 6.7); + yield return new Country("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 192.2, 0, 0, 66.98, 1400, 45.2, 15.9, 30.98, 9.31); + yield return new Country("Netherlands", "WESTERN EUROPE", 16491461, 41526, 397.1, 1.09, 2.91, 5.04, 28600, 99, 460.8, 10.9, 8.68); + yield return new Country("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 231, 37.92, -0.41, 10.03, 11400, 96.7, 365.3, 14.78, 6.45); + yield return new Country("New Caledonia", "OCEANIA", 219246, 19060, 11.5, 11.83, 0, 7.72, 15000, 91, 252.2, 18.11, 5.69); + yield return new Country("New Zealand", "OCEANIA", 4076140, 268680, 15.2, 5.63, 4.05, 5.85, 21600, 99, 441.7, 13.76, 7.53); + yield return new Country("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 43, 0.7, -1.22, 29.11, 2300, 67.5, 39.7, 24.51, 4.45); + yield return new Country("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 9.9, 0, -0.67, 121.69, 800, 17.6, 1.9, 50.73, 20.91); + yield return new Country("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 142.7, 0.09, 0.26, 98.8, 900, 68, 9.3, 40.43, 16.94); + yield return new Country("N. Mariana Islands", "OCEANIA", 82459, 477, 172.9, 310.69, 9.61, 7.11, 12500, 97, 254.7, 19.43, 2.29); + yield return new Country("Norway", "WESTERN EUROPE", 4610820, 323802, 14.2, 7.77, 1.74, 3.7, 37800, 100, 461.7, 11.46, 9.4); + yield return new Country("Oman", "NEAR EAST", 3102229, 212460, 14.6, 0.98, 0.28, 19.51, 13100, 75.8, 85.5, 36.24, 3.81); + yield return new Country("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 206.2, 0.13, -2.77, 72.44, 2100, 45.7, 31.8, 29.74, 8.23); + yield return new Country("Palau", "OCEANIA", 20579, 458, 44.9, 331.66, 2.85, 14.84, 9000, 92, 325.6, 18.03, 6.8); + yield return new Country("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 40.8, 3.18, -0.91, 20.47, 6300, 92.6, 137.9, 21.74, 5.36); + yield return new Country("Papua New Guinea", "OCEANIA", 5670544, 462840, 12.3, 1.11, 0, 51.45, 2200, 64.6, 10.9, 29.36, 7.25); + yield return new Country("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 16, 0, -0.08, 25.63, 4700, 94, 49.2, 29.1, 4.49); + yield return new Country("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 22, 0.19, -1.05, 31.94, 5100, 90.9, 79.5, 20.48, 6.23); + yield return new Country("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 298.2, 12.1, -1.5, 23.51, 4600, 92.6, 38.4, 24.89, 5.41); + yield return new Country("Poland", "EASTERN EUROPE", 38536869, 312685, 123.3, 0.16, -0.49, 8.51, 11100, 99.8, 306.3, 9.85, 9.89); + yield return new Country("Portugal", "WESTERN EUROPE", 10605870, 92391, 114.8, 1.94, 3.57, 5.05, 18000, 93.3, 399.2, 10.72, 10.5); + yield return new Country("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 284.8, 3.63, -1.46, 8.24, 16800, 94.1, 283.1, 12.77, 7.65); + yield return new Country("Qatar", "NEAR EAST", 885359, 11437, 77.4, 4.92, 16.29, 18.61, 21500, 82.5, 232, 15.56, 4.72); + yield return new Country("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 312.9, 8.22, 0, 7.78, 5800, 88.9, 380.9, 18.9, 5.49); + yield return new Country("Romania", "EASTERN EUROPE", 22303552, 237500, 93.9, 0.09, -0.13, 26.43, 7000, 98.4, 196.9, 10.7, 11.77); + yield return new Country("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8.4, 0.22, 1.02, 15.39, 8900, 99.6, 280.6, 9.95, 14.65); + yield return new Country("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 328.4, 0, 0, 91.23, 1300, 70.4, 2.7, 40.37, 16.09); + yield return new Country("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 18.2, 14.53, 0, 19, 2500, 97, 293.3, 12.13, 6.53); + yield return new Country("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 149.9, 51.72, -7.11, 14.49, 8800, 97, 638.9, 18.02, 8.33); + yield return new Country("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 273.5, 25.65, -2.67, 13.53, 5400, 67, 303.3, 19.68, 5.08); + yield return new Country("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 29, 49.59, -4.86, 7.54, 6900, 99, 683.2, 13.52, 6.83); + yield return new Country("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 303, 21.59, -7.64, 14.78, 2900, 96, 190.9, 16.18, 5.98); + yield return new Country("Samoa", "OCEANIA", 176908, 2944, 60.1, 13.69, -11.7, 27.71, 5600, 99.7, 75.2, 16.43, 6.62); + yield return new Country("San Marino", "WESTERN EUROPE", 29251, 61, 479.5, 0, 10.98, 5.73, 34600, 96, 704.3, 10.02, 8.17); + yield return new Country("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 193.2, 20.88, -2.72, 43.11, 1200, 79.3, 36.2, 40.25, 6.47); + yield return new Country("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 13.8, 0.13, -2.71, 13.24, 11800, 78.8, 140.6, 29.34, 2.58); + yield return new Country("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 61.1, 0.27, 0.2, 55.51, 1600, 40.2, 22.2, 32.78, 9.42); + yield return new Country("Serbia", "EASTERN EUROPE", 9396411, 88361, 106.3, 0, -1.33, 12.89, 2200, 93, 285.8,null,null); + yield return new Country("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 179.2, 107.91, -5.69, 15.53, 7800, 58, 262.4, 16.03, 6.29); + yield return new Country("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 83.7, 0.56, 0, 143.64, 500, 31.4, 4, 45.76, 23.03); + yield return new Country("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 6482.2, 27.85, 11.53, 2.29, 23700, 92.5, 411.4, 9.34, 4.28); + yield return new Country("Slovakia", "EASTERN EUROPE", 5439448, 48845, 111.4, 0, 0.3, 7.41, 13300,null, 220.1, 10.65, 9.45); + yield return new Country("Slovenia", "EASTERN EUROPE", 2010347, 20273, 99.2, 0.23, 1.12, 4.45, 19000, 99.7, 406.1, 8.98, 10.31); + yield return new Country("Solomon Islands", "OCEANIA", 552438, 28450, 19.4, 18.67, 0, 21.29, 1700,null, 13.4, 30.01, 3.92); + yield return new Country("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 13.9, 0.47, 5.37, 116.7, 500, 37.8, 11.3, 45.13, 16.63); + yield return new Country("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 36.2, 0.23, -0.29, 61.81, 10700, 86.4, 107, 18.2, 22); + yield return new Country("Spain", "WESTERN EUROPE", 40397842, 504782, 80, 0.98, 0.99, 4.42, 22000, 97.9, 453.5, 10.06, 9.72); + yield return new Country("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 308.2, 2.04, -1.31, 14.35, 3700, 92.3, 61.5, 15.51, 6.52); + yield return new Country("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 16.5, 0.03, -0.02, 62.5, 1900, 61.1, 16.3, 34.53, 8.97); + yield return new Country("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 2.7, 0.24, -8.81, 23.57, 4000, 93, 184.7, 18.02, 7.27); + yield return new Country("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 65.5, 0, 0, 69.27, 4900, 81.6, 30.8, 27.41, 29.74); + yield return new Country("Sweden", "WESTERN EUROPE", 9016596, 449964, 20, 0.72, 1.67, 2.77, 26800, 99, 715, 10.27, 10.31); + yield return new Country("Switzerland", "WESTERN EUROPE", 7523934, 41290, 182.2, 0, 4.05, 4.39, 32700, 99, 680.9, 9.71, 8.49); + yield return new Country("Syria", "NEAR EAST", 18881361, 185180, 102, 0.1, 0, 29.53, 3300, 76.9, 153.8, 27.76, 4.81); + yield return new Country("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 640.3, 4.35, 0, 6.4, 23400, 96.1, 591, 12.56, 6.48); + yield return new Country("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 51.2, 0, -2.86, 110.76, 1000, 99.4, 33.5, 32.65, 8.25); + yield return new Country("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 39.6, 0.15, -2.06, 98.54, 600, 78.2, 4, 37.71, 16.39); + yield return new Country("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 125.7, 0.63, 0, 20.48, 7400, 92.6, 108.9, 13.87, 7.04); + yield return new Country("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 97.7, 0.1, 0, 66.61, 1500, 60.9, 10.6, 37.01, 9.83); + yield return new Country("Tonga", "OCEANIA", 114689, 748, 153.3, 56.02, 0, 12.62, 2200, 98.5, 97.7, 25.37, 5.28); + yield return new Country("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 207.9, 7.06, -10.83, 24.31, 9500, 98.6, 303.5, 12.9, 10.57); + yield return new Country("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 62.2, 0.7, -0.57, 24.77, 6900, 74.2, 123.6, 15.52, 5.13); + yield return new Country("Turkey", "NEAR EAST", 70413958, 780580, 90.2, 0.92, 0, 41.04, 6700, 86.5, 269.5, 16.62, 5.97); + yield return new Country("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 10.3, 0, -0.86, 73.08, 5800, 98, 74.6, 27.61, 8.6); + yield return new Country("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 49.2, 90.47, 11.68, 15.67, 9600, 98, 269.5, 21.84, 4.21); + yield return new Country("Tuvalu", "OCEANIA", 11810, 26, 454.2, 92.31, 0, 20.03, 1100,null, 59.3, 22.18, 7.11); + yield return new Country("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 119.5, 0, 0, 67.83, 1400, 69.9, 3.6, 47.35, 12.24); + yield return new Country("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 77.4, 0.46, -0.39, 20.34, 5400, 99.7, 259.9, 8.82, 14.39); + yield return new Country("United Arab Emirates", "NEAR EAST", 2602713, 82880, 31.4, 1.59, 1.03, 14.51, 23200, 77.9, 475.3, 18.96, 4.4); + yield return new Country("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 247.6, 5.08, 2.19, 5.16, 27700, 99, 543.5, 10.71, 10.13); + yield return new Country("United States", "NORTHERN AMERICA", 298444215, 9631420, 31, 0.21, 3.41, 6.5, 37800, 97, 898, 14.14, 8.26); + yield return new Country("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 19.5, 0.37, -0.32, 11.95, 12800, 98, 291.4, 13.91, 9.05); + yield return new Country("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 61, 0, -1.72, 71.1, 1700, 99.3, 62.9, 26.36, 7.84); + yield return new Country("Vanuatu", "OCEANIA", 208869, 12200, 17.1, 20.72, 0, 55.16, 2900, 53, 32.6, 22.72, 7.82); + yield return new Country("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 28.2, 0.31, -0.04, 22.2, 4800, 93.4, 140.1, 18.71, 4.92); + yield return new Country("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 256.1, 1.05, -0.45, 25.95, 2500, 90.3, 187.7, 16.86, 6.22); + yield return new Country("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 56.9, 9.84, -8.94, 8.03, 17200,null, 652.8, 13.96, 6.43); + yield return new Country("Wallis and Futuna", "OCEANIA", 16025, 274, 58.5, 47.08,null,null, 3700, 50, 118.6,null,null); + yield return new Country("West Bank", "NEAR EAST", 2460492, 5860, 419.9, 0, 2.98, 19.62, 800,null, 145.2, 31.67, 3.92); + //yield return new Country("Western Sahara", "NORTHERN AFRICA", 273008, 266000, 1, 0.42,,,,,,,); + yield return new Country("Yemen", "NEAR EAST", 21456188, 527970, 40.6, 0.36, 0, 61.5, 800, 50.2, 37.2, 42.89, 8.3); + yield return new Country("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 15.3, 0, 0, 88.29, 800, 80.6, 8.2, 41, 19.93); + yield return new Country("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 31.3, 0, 0, 67.69, 1900, 90.7, 26.8, 28.01, 21.84); + + + } + + static IReadOnlyList _all; + + public static IReadOnlyList All + { + get + { + if(_all == null) + { + _all = GetCountries().ToList().AsReadOnly(); + } + + return _all; + } + + } + + /* + + + yield return new Country("Afghanistan","ASIA (EX. NEAR EAST)",31056997,647500,48,0,23.06,163.07,700,36,3.2,46.6,20.34); + yield return new Country("Albania","EASTERN EUROPE",3581655,28748,124.6,1.26,-4.93,21.52,4500,86.5,71.2,15.11,5.22); + yield return new Country("Algeria","NORTHERN AFRICA",32930091,2381740,13.8,0.04,-0.39,31,6000,70,78.1,17.14,4.61); + yield return new Country("American Samoa","OCEANIA",57794,199,290.4,58.29,-20.71,9.27,8000,97,259.5,22.46,3.27); + yield return new Country("Andorra","WESTERN EUROPE",71201,468,152.1,0,6.6,4.05,19000,100,497.2,8.71,6.25); + yield return new Country("Angola","SUB-SAHARAN AFRICA",12127071,1246700,9.7,0.13,0,191.19,1900,42,7.8,45.11,24.2); + yield return new Country("Anguilla","LATIN AMER. & CARIB",13477,102,132.1,59.8,10.76,21.03,8600,95,460,14.17,5.34); + yield return new Country("Antigua & Barbuda","LATIN AMER. & CARIB",69108,443,156,34.54,-6.15,19.46,11000,89,549.9,16.93,5.37); + yield return new Country("Argentina","LATIN AMER. & CARIB",39921833,2766890,14.4,0.18,0.61,15.18,11200,97.1,220.4,16.73,7.55); + yield return new Country("Armenia","C.W. OF IND. STATES",2976372,29800,99.9,0,-6.47,23.28,3500,98.6,195.7,12.07,8.23); + yield return new Country("Aruba","LATIN AMER. & CARIB",71891,193,372.5,35.49,0,5.89,28000,97,516.1,11.03,6.68); + yield return new Country("Australia","OCEANIA",20264082,7686850,2.6,0.34,3.98,4.69,29000,100,565.5,12.14,7.51); + yield return new Country("Austria","WESTERN EUROPE",8192880,83870,97.7,0,2,4.66,30000,98,452.2,8.74,9.76); + yield return new Country("Azerbaijan","C.W. OF IND. STATES",7961619,86600,91.9,0,-4.9,81.74,3400,97,137.1,20.74,9.75); + yield return new Country(""Bahamas"," The"",LATIN AMER. & CARIB,303770,13940,21.8,25.41,-2.2,25.21,16700,95.6,460.6,17.57,9.05); + yield return new Country("Bahrain","NEAR EAST",698585,665,1050.5,24.21,1.05,17.27,16900,89.1,281.3,17.8,4.14); + yield return new Country("Bangladesh","ASIA (EX. NEAR EAST)",147365352,144000,1023.4,0.4,-0.71,62.6,1900,43.1,7.3,29.8,8.27); + yield return new Country("Barbados","LATIN AMER. & CARIB",279912,431,649.5,22.51,-0.31,12.5,15700,97.4,481.9,12.71,8.67); + yield return new Country("Belarus","C.W. OF IND. STATES",10293011,207600,49.6,0,2.54,13.37,6100,99.6,319.1,11.16,14.02); + yield return new Country("Belgium","WESTERN EUROPE",10379067,30528,340,0.22,1.23,4.68,29100,98,462.6,10.38,10.27); + yield return new Country("Belize","LATIN AMER. & CARIB",287730,22966,12.5,1.68,0,25.69,4900,94.1,115.7,28.84,5.72); + yield return new Country("Benin","SUB-SAHARAN AFRICA",7862944,112620,69.8,0.11,0,85,1100,40.9,9.7,38.85,12.22); + yield return new Country("Bermuda","NORTHERN AMERICA",65773,53,1241,194.34,2.49,8.53,36000,98,851.4,11.4,7.74); + yield return new Country("Bhutan","ASIA (EX. NEAR EAST)",2279723,47000,48.5,0,0,100.44,1300,42.2,14.3,33.65,12.7); + yield return new Country("Bolivia","LATIN AMER. & CARIB",8989046,1098580,8.2,0,-1.32,53.11,2400,87.2,71.9,23.3,7.53); + yield return new Country("Bosnia & Herzegovina","EASTERN EUROPE",4498976,51129,88,0.04,0.31,21.05,6100,,215.4,8.77,8.27); + yield return new Country("Botswana","SUB-SAHARAN AFRICA",1639833,600370,2.7,0,0,54.58,9000,79.8,80.5,23.08,29.5); + yield return new Country("Brazil","LATIN AMER. & CARIB",188078227,8511965,22.1,0.09,-0.03,29.61,7600,86.4,225.3,16.56,6.17); + yield return new Country("British Virgin Is.","LATIN AMER. & CARIB",23098,153,151,52.29,10.01,18.05,16000,97.8,506.5,14.89,4.42); + yield return new Country("Brunei","ASIA (EX. NEAR EAST)",379444,5770,65.8,2.79,3.59,12.61,18600,93.9,237.2,18.79,3.45); + yield return new Country("Bulgaria","EASTERN EUROPE",7385367,110910,66.6,0.32,-4.58,20.55,7600,98.6,336.3,9.65,14.27); + yield return new Country("Burkina Faso","SUB-SAHARAN AFRICA",13902972,274200,50.7,0,0,97.57,1100,26.6,7,45.62,15.6); + yield return new Country("Burma","ASIA (EX. NEAR EAST)",47382633,678500,69.8,0.28,-1.8,67.24,1800,85.3,10.1,17.91,9.83); + yield return new Country("Burundi","SUB-SAHARAN AFRICA",8090068,27830,290.7,0,-0.06,69.29,600,51.6,3.4,42.22,13.46); + yield return new Country("Cambodia","ASIA (EX. NEAR EAST)",13881427,181040,76.7,0.24,0,71.48,1900,69.4,2.6,26.9,9.06); + yield return new Country("Cameroon","SUB-SAHARAN AFRICA",17340702,475440,36.5,0.08,0,68.26,1800,79,5.7,33.89,13.47); + yield return new Country("Canada","NORTHERN AMERICA",33098932,9984670,3.3,2.02,5.96,4.75,29800,97,552.2,10.78,7.8); + yield return new Country("Cape Verde","SUB-SAHARAN AFRICA",420979,4033,104.4,23.93,-12.07,47.77,1400,76.6,169.6,24.87,6.55); + yield return new Country("Cayman Islands","LATIN AMER. & CARIB",45436,262,173.4,61.07,18.75,8.19,35000,98,836.3,12.74,4.89); + yield return new Country("Central African Rep.","SUB-SAHARAN AFRICA",4303356,622984,6.9,0,0,91,1100,51,2.3,33.91,18.65); + yield return new Country("Chad","SUB-SAHARAN AFRICA",9944201,1284000,7.7,0,-0.11,93.82,1200,47.5,1.3,45.73,16.38); + yield return new Country("Chile","LATIN AMER. & CARIB",16134219,756950,21.3,0.85,0,8.8,9900,96.2,213,15.23,5.81); + yield return new Country("China","ASIA (EX. NEAR EAST)",1313973713,9596960,136.9,0.15,-0.4,24.18,5000,90.9,266.7,13.25,6.97); + yield return new Country("Colombia","LATIN AMER. & CARIB",43593035,1138910,38.3,0.28,-0.31,20.97,6300,92.5,176.2,20.48,5.58); + yield return new Country("Comoros","SUB-SAHARAN AFRICA",690948,2170,318.4,15.67,0,74.93,700,56.5,24.5,36.93,8.2); + yield return new Country(""Congo"," Dem. Rep."",SUB-SAHARAN AFRICA,62660551,2345410,26.7,0,0,94.69,700,65.5,0.2,43.69,13.27); + yield return new Country(""Congo"," Repub. of the"",SUB-SAHARAN AFRICA,3702314,342000,10.8,0.05,-0.17,93.86,700,83.8,3.7,42.57,12.93); + yield return new Country("Cook Islands","OCEANIA",21388,240,89.1,50,,,5000,95,289.9,21,); + yield return new Country("Costa Rica","LATIN AMER. & CARIB",4075261,51100,79.8,2.52,0.51,9.95,9100,96,340.7,18.32,4.36); + yield return new Country("Cote d'Ivoire","SUB-SAHARAN AFRICA",17654843,322460,54.8,0.16,-0.07,90.83,1400,50.9,14.6,35.11,14.84); + yield return new Country("Croatia","EASTERN EUROPE",4494749,56542,79.5,10.32,1.58,6.84,10600,98.5,420.4,9.61,11.48); + yield return new Country("Cuba","LATIN AMER. & CARIB",11382820,110860,102.7,3.37,-1.58,6.33,2900,97,74.7,11.89,7.22); + yield return new Country("Cyprus","NEAR EAST",784301,9250,84.8,7.01,0.43,7.18,19200,97.6,,12.56,7.68); + yield return new Country("Czech Republic","EASTERN EUROPE",10235455,78866,129.8,0,0.97,3.93,15700,99.9,314.3,9.02,10.59); + yield return new Country("Denmark","WESTERN EUROPE",5450661,43094,126.5,16.97,2.48,4.56,31100,100,614.6,11.13,10.36); + yield return new Country("Djibouti","SUB-SAHARAN AFRICA",486530,23000,21.2,1.37,0,104.13,1300,67.9,22.8,39.53,19.31); + yield return new Country("Dominica","LATIN AMER. & CARIB",68910,754,91.4,19.63,-13.87,14.15,5400,94,304.8,15.27,6.73); + yield return new Country("Dominican Republic","LATIN AMER. & CARIB",9183984,48730,188.5,2.64,-3.22,32.38,6000,84.7,97.4,23.22,5.73); + yield return new Country("East Timor","ASIA (EX. NEAR EAST)",1062777,15007,70.8,4.7,0,47.41,500,58.6,,26.99,6.24); + yield return new Country("Ecuador","LATIN AMER. & CARIB",13547510,283560,47.8,0.79,-8.58,23.66,3300,92.5,125.6,22.29,4.23); + yield return new Country("Egypt","NORTHERN AFRICA",78887007,1001450,78.8,0.24,-0.22,32.59,4000,57.7,131.8,22.94,5.23); + yield return new Country("El Salvador","LATIN AMER. & CARIB",6822378,21040,324.3,1.46,-3.74,25.1,4800,80.2,142.4,26.61,5.78); + yield return new Country("Equatorial Guinea","SUB-SAHARAN AFRICA",540109,28051,19.3,1.06,0,85.13,2700,85.7,18.5,35.59,15.06); + yield return new Country("Eritrea","SUB-SAHARAN AFRICA",4786994,121320,39.5,1.84,0,74.87,700,58.6,7.9,34.33,9.6); + yield return new Country("Estonia","BALTICS",1324333,45226,29.3,8.39,-3.16,7.87,12300,99.8,333.8,10.04,13.25); + yield return new Country("Ethiopia","SUB-SAHARAN AFRICA",74777981,1127127,66.3,0,0,95.32,700,42.7,8.2,37.98,14.86); + yield return new Country("Faroe Islands","WESTERN EUROPE",47246,1399,33.8,79.84,1.41,6.24,22000,,503.8,14.05,8.7); + yield return new Country("Fiji","OCEANIA",905949,18270,49.6,6.18,-3.14,12.62,5800,93.7,112.6,22.55,5.65); + yield return new Country("Finland","WESTERN EUROPE",5231372,338145,15.5,0.37,0.95,3.57,27400,100,405.3,10.45,9.86); + yield return new Country("France","WESTERN EUROPE",60876136,547030,111.3,0.63,0.66,4.26,27600,99,586.4,11.99,9.14); + yield return new Country("French Guiana","LATIN AMER. & CARIB",199509,91000,2.2,0.42,6.27,12.07,8300,83,255.6,20.46,4.88); + yield return new Country("French Polynesia","OCEANIA",274578,4167,65.9,60.6,2.94,8.44,17500,98,194.5,16.68,4.69); + yield return new Country("Gabon","SUB-SAHARAN AFRICA",1424906,267667,5.3,0.33,0,53.64,5500,63.2,27.4,36.16,12.25); + yield return new Country(""Gambia"," The"",SUB-SAHARAN AFRICA,1641564,11300,145.3,0.71,1.57,72.02,1700,40.1,26.8,39.37,12.25); + yield return new Country("Gaza Strip","NEAR EAST",1428757,360,3968.8,11.11,1.6,22.93,600,,244.3,39.45,3.8); + yield return new Country("Georgia","C.W. OF IND. STATES",4661473,69700,66.9,0.44,-4.7,18.59,2500,99,146.6,10.41,9.23); + yield return new Country("Germany","WESTERN EUROPE",82422299,357021,230.9,0.67,2.18,4.16,27600,99,667.9,8.25,10.62); + yield return new Country("Ghana","SUB-SAHARAN AFRICA",22409572,239460,93.6,0.23,-0.64,51.43,2200,74.8,14.4,30.52,9.72); + yield return new Country("Gibraltar","WESTERN EUROPE",27928,7,3989.7,171.43,0,5.13,17500,,877.7,10.74,9.31); + yield return new Country("Greece","WESTERN EUROPE",10688058,131940,81,10.37,2.35,5.53,20000,97.5,589.7,9.68,10.24); + yield return new Country("Greenland","NORTHERN AMERICA",56361,2166086,0,2.04,-8.37,15.82,20000,,448.9,15.93,7.84); + yield return new Country("Grenada","LATIN AMER. & CARIB",89703,344,260.8,35.17,-13.92,14.62,5000,98,364.5,22.08,6.88); + yield return new Country("Guadeloupe","LATIN AMER. & CARIB",452776,1780,254.4,17.19,-0.15,8.6,8000,90,463.8,15.05,6.09); + yield return new Country("Guam","OCEANIA",171019,541,316.1,23.2,0,6.94,21000,99,492,18.79,4.48); + yield return new Country("Guatemala","LATIN AMER. & CARIB",12293545,108890,112.9,0.37,-1.67,35.93,4100,70.6,92.1,29.88,5.2); + yield return new Country("Guernsey","WESTERN EUROPE",65409,78,838.6,64.1,3.84,4.71,20000,,842.4,8.81,10.01); + yield return new Country("Guinea","SUB-SAHARAN AFRICA",9690222,245857,39.4,0.13,-3.06,90.37,2100,35.9,2.7,41.76,15.48); + yield return new Country("Guinea-Bissau","SUB-SAHARAN AFRICA",1442029,36120,39.9,0.97,-1.57,107.17,800,42.4,7.4,37.22,16.53); + yield return new Country("Guyana","LATIN AMER. & CARIB",767245,214970,3.6,0.21,-2.07,33.26,4000,98.8,143.5,18.28,8.28); + yield return new Country("Haiti","LATIN AMER. & CARIB",8308504,27750,299.4,6.38,-3.4,73.45,1600,52.9,16.9,36.44,12.17); + yield return new Country("Honduras","LATIN AMER. & CARIB",7326496,112090,65.4,0.73,-1.99,29.32,2600,76.2,67.5,28.24,5.28); + yield return new Country("Hong Kong","ASIA (EX. NEAR EAST)",6940432,1092,6355.7,67.12,5.24,2.97,28800,93.5,546.7,7.29,6.29); + yield return new Country("Hungary","EASTERN EUROPE",9981334,93030,107.3,0,0.86,8.57,13900,99.4,336.2,9.72,13.11); + yield return new Country("Iceland","WESTERN EUROPE",299388,103000,2.9,4.83,2.38,3.31,30900,99.9,647.7,13.64,6.72); + yield return new Country("India","ASIA (EX. NEAR EAST)",1095351995,3287590,333.2,0.21,-0.07,56.29,2900,59.5,45.4,22.01,8.18); + yield return new Country("Indonesia","ASIA (EX. NEAR EAST)",245452739,1919440,127.9,2.85,0,35.6,3200,87.9,52,20.34,6.25); + yield return new Country("Iran","ASIA (EX. NEAR EAST)",68688433,1648000,41.7,0.15,-0.84,41.58,7000,79.4,276.4,17,5.55); + yield return new Country("Iraq","NEAR EAST",26783383,437072,61.3,0.01,0,50.25,1500,40.4,38.6,31.98,5.37); + yield return new Country("Ireland","WESTERN EUROPE",4062235,70280,57.8,2.06,4.99,5.39,29600,98,500.5,14.45,7.82); + yield return new Country("Isle of Man","WESTERN EUROPE",75441,572,131.9,27.97,5.36,5.93,21000,,676,11.05,11.19); + yield return new Country("Israel","NEAR EAST",6352117,20770,305.8,1.31,0.68,7.03,19800,95.4,462.3,17.97,6.18); + yield return new Country("Italy","WESTERN EUROPE",58133509,301230,193,2.52,2.07,5.94,26700,98.6,430.9,8.72,10.4); + yield return new Country("Jamaica","LATIN AMER. & CARIB",2758124,10991,250.9,9.3,-4.92,12.36,3900,87.9,124,20.82,6.52); + yield return new Country("Japan","ASIA (EX. NEAR EAST)",127463611,377835,337.4,7.87,0,3.26,28200,99,461.2,9.37,9.16); + yield return new Country("Jersey","WESTERN EUROPE",91084,116,785.2,60.34,2.76,5.24,24800,,811.3,9.3,9.28); + yield return new Country("Jordan","NEAR EAST",5906760,92300,64,0.03,6.59,17.35,4300,91.3,104.5,21.25,2.65); + yield return new Country("Kazakhstan","C.W. OF IND. STATES",15233244,2717300,5.6,0,-3.35,29.21,6300,98.4,164.1,16,9.42); + yield return new Country("Kenya","SUB-SAHARAN AFRICA",34707817,582650,59.6,0.09,-0.1,61.47,1000,85.1,8.1,39.72,14.02); + yield return new Country("Kiribati","OCEANIA",105432,811,130,140.94,0,48.52,800,,42.7,30.65,8.26); + yield return new Country(""Korea"," North"",ASIA (EX. NEAR EAST),23113019,120540,191.8,2.07,0,24.04,1300,99,42.4,15.54,7.13); + yield return new Country(""Korea"," South"",ASIA (EX. NEAR EAST),48846823,98480,496,2.45,0,7.05,17800,97.9,486.1,10,5.85); + yield return new Country("Kuwait","NEAR EAST",2418393,17820,135.7,2.8,14.18,9.95,19000,83.5,211,21.94,2.41); + yield return new Country("Kyrgyzstan","C.W. OF IND. STATES",5213898,198500,26.3,0,-2.45,35.64,1600,97,84,22.8,7.08); + yield return new Country("Laos","ASIA (EX. NEAR EAST)",6368481,236800,26.9,0,0,85.22,1700,66.4,14.1,35.49,11.55); + yield return new Country("Latvia","BALTICS",2274735,64589,35.2,0.82,-2.23,9.55,10200,99.8,321.4,9.24,13.66); + yield return new Country("Lebanon","NEAR EAST",3874050,10400,372.5,2.16,0,24.52,4800,87.4,255.6,18.52,6.21); + yield return new Country("Lesotho","SUB-SAHARAN AFRICA",2022331,30355,66.6,0,-0.74,84.23,3000,84.8,23.7,24.75,28.71); + yield return new Country("Liberia","SUB-SAHARAN AFRICA",3042004,111370,27.3,0.52,0,128.87,1000,57.5,2.3,44.77,23.1); + yield return new Country("Libya","NORTHERN AFRICA",5900754,1759540,3.4,0.1,0,24.6,6400,82.6,127.1,26.49,3.48); + yield return new Country("Liechtenstein","WESTERN EUROPE",33987,160,212.4,0,4.85,4.7,25000,100,585.5,10.21,7.18); + yield return new Country("Lithuania","BALTICS",3585906,65200,55,0.14,-0.71,6.89,11400,99.6,223.4,8.75,10.98); + yield return new Country("Luxembourg","WESTERN EUROPE",474413,2586,183.5,0,8.97,4.81,55100,100,515.4,11.94,8.41); + yield return new Country("Macau","ASIA (EX. NEAR EAST)",453125,28,16183,146.43,4.86,4.39,19400,94.5,384.9,8.48,4.47); + yield return new Country("Macedonia","EASTERN EUROPE",2050554,25333,80.9,0,-1.45,10.09,6700,,260,12.02,8.77); + yield return new Country("Madagascar","SUB-SAHARAN AFRICA",18595469,587040,31.7,0.82,0,76.83,800,68.9,3.6,41.41,11.11); + yield return new Country("Malawi","SUB-SAHARAN AFRICA",13013926,118480,109.8,0,0,103.32,600,62.7,7.9,43.13,19.33); + yield return new Country("Malaysia","ASIA (EX. NEAR EAST)",24385858,329750,74,1.42,0,17.7,9000,88.7,179,22.86,5.05); + yield return new Country("Maldives","ASIA (EX. NEAR EAST)",359008,300,1196.7,214.67,0,56.52,3900,97.2,90,34.81,7.06); + yield return new Country("Mali","SUB-SAHARAN AFRICA",11716829,1240000,9.5,0,-0.33,116.79,900,46.4,6.4,49.82,16.89); + yield return new Country("Malta","WESTERN EUROPE",400214,316,1266.5,62.28,2.07,3.89,17700,92.8,505,10.22,8.1); + yield return new Country("Marshall Islands","OCEANIA",60422,11854,5.1,3.12,-6.04,29.45,1600,93.7,91.2,33.05,4.78); + yield return new Country("Martinique","LATIN AMER. & CARIB",436131,1100,396.5,31.82,-0.05,7.09,14400,97.7,394.4,13.74,6.48); + yield return new Country("Mauritania","SUB-SAHARAN AFRICA",3177388,1030700,3.1,0.07,0,70.89,1800,41.7,12.9,40.99,12.16); + yield return new Country("Mauritius","SUB-SAHARAN AFRICA",1240827,2040,608.3,8.68,-0.9,15.03,11400,85.6,289.3,15.43,6.86); + yield return new Country("Mayotte","SUB-SAHARAN AFRICA",201234,374,538.1,49.52,6.78,62.4,2600,,49.7,40.95,7.7); + yield return new Country("Mexico","LATIN AMER. & CARIB",107449525,1972550,54.5,0.47,-4.87,20.91,9000,92.2,181.6,20.69,4.74); + yield return new Country(""Micronesia"," Fed. St."",OCEANIA,108004,702,153.9,870.66,-20.99,30.21,2000,89,114.8,24.68,4.75); + yield return new Country("Moldova","C.W. OF IND. STATES",4466706,33843,132,0,-0.26,40.42,1800,99.1,208.1,15.7,12.64); + yield return new Country("Monaco","WESTERN EUROPE",32543,2,16271.5,205,7.75,5.43,27000,99,1035.6,9.19,12.91); + yield return new Country("Mongolia","ASIA (EX. NEAR EAST)",2832224,1564116,1.8,0,0,53.79,1800,97.8,55.1,21.59,6.95); + yield return new Country("Montserrat","LATIN AMER. & CARIB",9439,102,92.5,39.22,0,7.35,3400,97,,17.59,7.1); + yield return new Country("Morocco","NORTHERN AFRICA",33241259,446550,74.4,0.41,-0.98,41.62,4000,51.7,40.4,21.98,5.58); + yield return new Country("Mozambique","SUB-SAHARAN AFRICA",19686505,801590,24.6,0.31,0,130.79,1200,47.8,3.5,35.18,21.35); + yield return new Country("Namibia","SUB-SAHARAN AFRICA",2044147,825418,2.5,0.19,0,48.98,7200,84,62.6,24.32,18.86); + yield return new Country("Nauru","OCEANIA",13287,21,632.7,142.86,0,9.95,5000,,143,24.76,6.7); + yield return new Country("Nepal","ASIA (EX. NEAR EAST)",28287147,147181,192.2,0,0,66.98,1400,45.2,15.9,30.98,9.31); + yield return new Country("Netherlands","WESTERN EUROPE",16491461,41526,397.1,1.09,2.91,5.04,28600,99,460.8,10.9,8.68); + yield return new Country("Netherlands Antilles","LATIN AMER. & CARIB",221736,960,231,37.92,-0.41,10.03,11400,96.7,365.3,14.78,6.45); + yield return new Country("New Caledonia","OCEANIA",219246,19060,11.5,11.83,0,7.72,15000,91,252.2,18.11,5.69); + yield return new Country("New Zealand","OCEANIA",4076140,268680,15.2,5.63,4.05,5.85,21600,99,441.7,13.76,7.53); + yield return new Country("Nicaragua","LATIN AMER. & CARIB",5570129,129494,43,0.7,-1.22,29.11,2300,67.5,39.7,24.51,4.45); + yield return new Country("Niger","SUB-SAHARAN AFRICA",12525094,1267000,9.9,0,-0.67,121.69,800,17.6,1.9,50.73,20.91); + yield return new Country("Nigeria","SUB-SAHARAN AFRICA",131859731,923768,142.7,0.09,0.26,98.8,900,68,9.3,40.43,16.94); + yield return new Country("N. Mariana Islands","OCEANIA",82459,477,172.9,310.69,9.61,7.11,12500,97,254.7,19.43,2.29); + yield return new Country("Norway","WESTERN EUROPE",4610820,323802,14.2,7.77,1.74,3.7,37800,100,461.7,11.46,9.4); + yield return new Country("Oman","NEAR EAST",3102229,212460,14.6,0.98,0.28,19.51,13100,75.8,85.5,36.24,3.81); + yield return new Country("Pakistan","ASIA (EX. NEAR EAST)",165803560,803940,206.2,0.13,-2.77,72.44,2100,45.7,31.8,29.74,8.23); + yield return new Country("Palau","OCEANIA",20579,458,44.9,331.66,2.85,14.84,9000,92,325.6,18.03,6.8); + yield return new Country("Panama","LATIN AMER. & CARIB",3191319,78200,40.8,3.18,-0.91,20.47,6300,92.6,137.9,21.74,5.36); + yield return new Country("Papua New Guinea","OCEANIA",5670544,462840,12.3,1.11,0,51.45,2200,64.6,10.9,29.36,7.25); + yield return new Country("Paraguay","LATIN AMER. & CARIB",6506464,406750,16,0,-0.08,25.63,4700,94,49.2,29.1,4.49); + yield return new Country("Peru","LATIN AMER. & CARIB",28302603,1285220,22,0.19,-1.05,31.94,5100,90.9,79.5,20.48,6.23); + yield return new Country("Philippines","ASIA (EX. NEAR EAST)",89468677,300000,298.2,12.1,-1.5,23.51,4600,92.6,38.4,24.89,5.41); + yield return new Country("Poland","EASTERN EUROPE",38536869,312685,123.3,0.16,-0.49,8.51,11100,99.8,306.3,9.85,9.89); + yield return new Country("Portugal","WESTERN EUROPE",10605870,92391,114.8,1.94,3.57,5.05,18000,93.3,399.2,10.72,10.5); + yield return new Country("Puerto Rico","LATIN AMER. & CARIB",3927188,13790,284.8,3.63,-1.46,8.24,16800,94.1,283.1,12.77,7.65); + yield return new Country("Qatar","NEAR EAST",885359,11437,77.4,4.92,16.29,18.61,21500,82.5,232,15.56,4.72); + yield return new Country("Reunion","SUB-SAHARAN AFRICA",787584,2517,312.9,8.22,0,7.78,5800,88.9,380.9,18.9,5.49); + yield return new Country("Romania","EASTERN EUROPE",22303552,237500,93.9,0.09,-0.13,26.43,7000,98.4,196.9,10.7,11.77); + yield return new Country("Russia","C.W. OF IND. STATES",142893540,17075200,8.4,0.22,1.02,15.39,8900,99.6,280.6,9.95,14.65); + yield return new Country("Rwanda","SUB-SAHARAN AFRICA",8648248,26338,328.4,0,0,91.23,1300,70.4,2.7,40.37,16.09); + yield return new Country("Saint Helena","SUB-SAHARAN AFRICA",7502,413,18.2,14.53,0,19,2500,97,293.3,12.13,6.53); + yield return new Country("Saint Kitts & Nevis","LATIN AMER. & CARIB",39129,261,149.9,51.72,-7.11,14.49,8800,97,638.9,18.02,8.33); + yield return new Country("Saint Lucia","LATIN AMER. & CARIB",168458,616,273.5,25.65,-2.67,13.53,5400,67,303.3,19.68,5.08); + yield return new Country("St Pierre & Miquelon","NORTHERN AMERICA",7026,242,29,49.59,-4.86,7.54,6900,99,683.2,13.52,6.83); + yield return new Country("Saint Vincent and the Grenadines","LATIN AMER. & CARIB",117848,389,303,21.59,-7.64,14.78,2900,96,190.9,16.18,5.98); + yield return new Country("Samoa","OCEANIA",176908,2944,60.1,13.69,-11.7,27.71,5600,99.7,75.2,16.43,6.62); + yield return new Country("San Marino","WESTERN EUROPE",29251,61,479.5,0,10.98,5.73,34600,96,704.3,10.02,8.17); + yield return new Country("Sao Tome & Principe","SUB-SAHARAN AFRICA",193413,1001,193.2,20.88,-2.72,43.11,1200,79.3,36.2,40.25,6.47); + yield return new Country("Saudi Arabia","NEAR EAST",27019731,1960582,13.8,0.13,-2.71,13.24,11800,78.8,140.6,29.34,2.58); + yield return new Country("Senegal","SUB-SAHARAN AFRICA",11987121,196190,61.1,0.27,0.2,55.51,1600,40.2,22.2,32.78,9.42); + yield return new Country("Serbia","EASTERN EUROPE",9396411,88361,106.3,0,-1.33,12.89,2200,93,285.8,,); + yield return new Country("Seychelles","SUB-SAHARAN AFRICA",81541,455,179.2,107.91,-5.69,15.53,7800,58,262.4,16.03,6.29); + yield return new Country("Sierra Leone","SUB-SAHARAN AFRICA",6005250,71740,83.7,0.56,0,143.64,500,31.4,4,45.76,23.03); + yield return new Country("Singapore","ASIA (EX. NEAR EAST)",4492150,693,6482.2,27.85,11.53,2.29,23700,92.5,411.4,9.34,4.28); + yield return new Country("Slovakia","EASTERN EUROPE",5439448,48845,111.4,0,0.3,7.41,13300,,220.1,10.65,9.45); + yield return new Country("Slovenia","EASTERN EUROPE",2010347,20273,99.2,0.23,1.12,4.45,19000,99.7,406.1,8.98,10.31); + yield return new Country("Solomon Islands","OCEANIA",552438,28450,19.4,18.67,0,21.29,1700,,13.4,30.01,3.92); + yield return new Country("Somalia","SUB-SAHARAN AFRICA",8863338,637657,13.9,0.47,5.37,116.7,500,37.8,11.3,45.13,16.63); + yield return new Country("South Africa","SUB-SAHARAN AFRICA",44187637,1219912,36.2,0.23,-0.29,61.81,10700,86.4,107,18.2,22); + yield return new Country("Spain","WESTERN EUROPE",40397842,504782,80,0.98,0.99,4.42,22000,97.9,453.5,10.06,9.72); + yield return new Country("Sri Lanka","ASIA (EX. NEAR EAST)",20222240,65610,308.2,2.04,-1.31,14.35,3700,92.3,61.5,15.51,6.52); + yield return new Country("Sudan","SUB-SAHARAN AFRICA",41236378,2505810,16.5,0.03,-0.02,62.5,1900,61.1,16.3,34.53,8.97); + yield return new Country("Suriname","LATIN AMER. & CARIB",439117,163270,2.7,0.24,-8.81,23.57,4000,93,184.7,18.02,7.27); + yield return new Country("Swaziland","SUB-SAHARAN AFRICA",1136334,17363,65.5,0,0,69.27,4900,81.6,30.8,27.41,29.74); + yield return new Country("Sweden","WESTERN EUROPE",9016596,449964,20,0.72,1.67,2.77,26800,99,715,10.27,10.31); + yield return new Country("Switzerland","WESTERN EUROPE",7523934,41290,182.2,0,4.05,4.39,32700,99,680.9,9.71,8.49); + yield return new Country("Syria","NEAR EAST",18881361,185180,102,0.1,0,29.53,3300,76.9,153.8,27.76,4.81); + yield return new Country("Taiwan","ASIA (EX. NEAR EAST)",23036087,35980,640.3,4.35,0,6.4,23400,96.1,591,12.56,6.48); + yield return new Country("Tajikistan","C.W. OF IND. STATES",7320815,143100,51.2,0,-2.86,110.76,1000,99.4,33.5,32.65,8.25); + yield return new Country("Tanzania","SUB-SAHARAN AFRICA",37445392,945087,39.6,0.15,-2.06,98.54,600,78.2,4,37.71,16.39); + yield return new Country("Thailand","ASIA (EX. NEAR EAST)",64631595,514000,125.7,0.63,0,20.48,7400,92.6,108.9,13.87,7.04); + yield return new Country("Togo","SUB-SAHARAN AFRICA",5548702,56785,97.7,0.1,0,66.61,1500,60.9,10.6,37.01,9.83); + yield return new Country("Tonga","OCEANIA",114689,748,153.3,56.02,0,12.62,2200,98.5,97.7,25.37,5.28); + yield return new Country("Trinidad & Tobago","LATIN AMER. & CARIB",1065842,5128,207.9,7.06,-10.83,24.31,9500,98.6,303.5,12.9,10.57); + yield return new Country("Tunisia","NORTHERN AFRICA",10175014,163610,62.2,0.7,-0.57,24.77,6900,74.2,123.6,15.52,5.13); + yield return new Country("Turkey","NEAR EAST",70413958,780580,90.2,0.92,0,41.04,6700,86.5,269.5,16.62,5.97); + yield return new Country("Turkmenistan","C.W. OF IND. STATES",5042920,488100,10.3,0,-0.86,73.08,5800,98,74.6,27.61,8.6); + yield return new Country("Turks & Caicos Is","LATIN AMER. & CARIB",21152,430,49.2,90.47,11.68,15.67,9600,98,269.5,21.84,4.21); + yield return new Country("Tuvalu","OCEANIA",11810,26,454.2,92.31,0,20.03,1100,,59.3,22.18,7.11); + yield return new Country("Uganda","SUB-SAHARAN AFRICA",28195754,236040,119.5,0,0,67.83,1400,69.9,3.6,47.35,12.24); + yield return new Country("Ukraine","C.W. OF IND. STATES",46710816,603700,77.4,0.46,-0.39,20.34,5400,99.7,259.9,8.82,14.39); + yield return new Country("United Arab Emirates","NEAR EAST",2602713,82880,31.4,1.59,1.03,14.51,23200,77.9,475.3,18.96,4.4); + yield return new Country("United Kingdom","WESTERN EUROPE",60609153,244820,247.6,5.08,2.19,5.16,27700,99,543.5,10.71,10.13); + yield return new Country("United States","NORTHERN AMERICA",298444215,9631420,31,0.21,3.41,6.5,37800,97,898,14.14,8.26); + yield return new Country("Uruguay","LATIN AMER. & CARIB",3431932,176220,19.5,0.37,-0.32,11.95,12800,98,291.4,13.91,9.05); + yield return new Country("Uzbekistan","C.W. OF IND. STATES",27307134,447400,61,0,-1.72,71.1,1700,99.3,62.9,26.36,7.84); + yield return new Country("Vanuatu","OCEANIA",208869,12200,17.1,20.72,0,55.16,2900,53,32.6,22.72,7.82); + yield return new Country("Venezuela","LATIN AMER. & CARIB",25730435,912050,28.2,0.31,-0.04,22.2,4800,93.4,140.1,18.71,4.92); + yield return new Country("Vietnam","ASIA (EX. NEAR EAST)",84402966,329560,256.1,1.05,-0.45,25.95,2500,90.3,187.7,16.86,6.22); + yield return new Country("Virgin Islands","LATIN AMER. & CARIB",108605,1910,56.9,9.84,-8.94,8.03,17200,,652.8,13.96,6.43); + yield return new Country("Wallis and Futuna","OCEANIA",16025,274,58.5,47.08,,,3700,50,118.6,,); + yield return new Country("West Bank","NEAR EAST",2460492,5860,419.9,0,2.98,19.62,800,,145.2,31.67,3.92); + yield return new Country("Western Sahara","NORTHERN AFRICA",273008,266000,1,0.42,,,,,,,); + yield return new Country("Yemen","NEAR EAST",21456188,527970,40.6,0.36,0,61.5,800,50.2,37.2,42.89,8.3); + yield return new Country("Zambia","SUB-SAHARAN AFRICA",11502010,752614,15.3,0,0,88.29,800,80.6,8.2,41,19.93); + yield return new Country("Zimbabwe","SUB-SAHARAN AFRICA",12236805,390580,31.3,0,0,67.69,1900,90.7,26.8,28.01,21.84); + + + */ + + } +} diff --git a/samples/DataGridSample/DataGridSample.csproj b/samples/DataGridSample/DataGridSample.csproj new file mode 100644 index 0000000000..d5ce1fd88b --- /dev/null +++ b/samples/DataGridSample/DataGridSample.csproj @@ -0,0 +1,22 @@ + + + Exe + netcoreapp2.0;net461 + + + + %(Filename) + + + Designer + + + + + + + + + + + \ No newline at end of file diff --git a/samples/DataGridSample/MainWindow.xaml b/samples/DataGridSample/MainWindow.xaml new file mode 100644 index 0000000000..6dae1da8d0 --- /dev/null +++ b/samples/DataGridSample/MainWindow.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/DataGridSample/MainWindow.xaml.cs b/samples/DataGridSample/MainWindow.xaml.cs new file mode 100644 index 0000000000..6a172f3afc --- /dev/null +++ b/samples/DataGridSample/MainWindow.xaml.cs @@ -0,0 +1,168 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Media; + +namespace DataGridSample +{ + public class Person : System.ComponentModel.INotifyDataErrorInfo, INotifyPropertyChanged + { + string _firstName; + string _lastName; + + public string FirstName + { + get => _firstName; + set + { + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(FirstName), "First Name Required"); + else + SetError(nameof(FirstName), null); + + _firstName = value; + OnPropertyChanged(nameof(FirstName)); + } + + } + + public string LastName + { + get => _lastName; + set + { + if (string.IsNullOrWhiteSpace(value)) + SetError(nameof(LastName), "Last Name Required"); + else + SetError(nameof(LastName), null); + + _lastName = value; + OnPropertyChanged(nameof(LastName)); + } + } + + Dictionary> _errorLookup = new Dictionary>(); + + void SetError(string propertyName, string error) + { + if (string.IsNullOrEmpty(error)) + { + if (_errorLookup.Remove(propertyName)) + OnErrorsChanged(propertyName); + } + else + { + if (_errorLookup.TryGetValue(propertyName, out List errorList)) + { + errorList.Clear(); + errorList.Add(error); + } + else + { + var errors = new List { error }; + _errorLookup.Add(propertyName, errors); + } + + OnErrorsChanged(propertyName); + } + } + + public bool HasErrors => _errorLookup.Count > 0; + + public event EventHandler ErrorsChanged; + public event PropertyChangedEventHandler PropertyChanged; + + void OnErrorsChanged(string propertyName) + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public IEnumerable GetErrors(string propertyName) + { + if (_errorLookup.TryGetValue(propertyName, out List errorList)) + return errorList; + else + return null; + } + } + + public class GDPValueConverter : Avalonia.Data.Converters.IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is int gdp) + { + if (gdp <= 5000) + return Brushes.Orange; + else if (gdp <= 10000) + return Brushes.Yellow; + else + return Brushes.LightGreen; + } + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + var dg1 = this.FindControl("dataGrid1"); + dg1.IsReadOnly = true; + + var collectionView1 = new Avalonia.Collections.CollectionViewBase(Countries.All); + //collectionView.GroupDescriptions.Add(new Avalonia.Collections.PathGroupDescription("Region")); + + dg1.Items = collectionView1; + + var dg2 = this.FindControl("dataGridGrouping"); + dg2.IsReadOnly = true; + + var collectionView2 = new Avalonia.Collections.CollectionViewBase(Countries.All); + collectionView2.GroupDescriptions.Add(new Avalonia.Collections.PathGroupDescription("Region")); + + dg2.Items = collectionView2; + + var dg3 = this.FindControl("dataGridEdit"); + dg3.IsReadOnly = false; + + var items = new List + { + new Person { FirstName = "John", LastName = "Doe" }, + new Person { FirstName = "Elizabeth", LastName = "Thomas" }, + new Person { FirstName = "Zack", LastName = "Ward" } + }; + var collectionView3 = new Avalonia.Collections.CollectionViewBase(items); + + dg3.Items = collectionView3; + + var addButton = this.FindControl("btnAdd"); + addButton.Click += (a, b) => collectionView3.AddNew(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/DataGridSample/Program.cs b/samples/DataGridSample/Program.cs new file mode 100644 index 0000000000..ef1ac68608 --- /dev/null +++ b/samples/DataGridSample/Program.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia; +using Avalonia.Logging.Serilog; + +namespace DataGridSample +{ + class Program + { + static void Main(string[] args) + { + BuildAvaloniaApp().Start(); + } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToDebug(); + } +} diff --git a/src/Avalonia.DataGrid.Themes.Default/Avalonia.DataGrid.Themes.Default.csproj b/src/Avalonia.DataGrid.Themes.Default/Avalonia.DataGrid.Themes.Default.csproj new file mode 100644 index 0000000000..d6e449a854 --- /dev/null +++ b/src/Avalonia.DataGrid.Themes.Default/Avalonia.DataGrid.Themes.Default.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + + + + + + + + %(Filename) + + + Designer + + + + + + + + + + + MSBuild:Compile + + + diff --git a/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml b/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml new file mode 100644 index 0000000000..47e05bd72a --- /dev/null +++ b/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml @@ -0,0 +1,927 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml.cs b/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml.cs new file mode 100644 index 0000000000..e57e6d5ce0 --- /dev/null +++ b/src/Avalonia.DataGrid.Themes.Default/DefaultTheme.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Avalonia.DataGrid.Themes.Default +{ + public class DefaultTheme : Styles + { + public DefaultTheme() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.DataGrid/Avalonia.DataGrid.csproj b/src/Avalonia.DataGrid/Avalonia.DataGrid.csproj new file mode 100644 index 0000000000..b0189a4994 --- /dev/null +++ b/src/Avalonia.DataGrid/Avalonia.DataGrid.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Avalonia.DataGrid/Collections/AvaloniaGroupDescription.cs b/src/Avalonia.DataGrid/Collections/AvaloniaGroupDescription.cs new file mode 100644 index 0000000000..fac0869667 --- /dev/null +++ b/src/Avalonia.DataGrid/Collections/AvaloniaGroupDescription.cs @@ -0,0 +1,1375 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public abstract class AvaloniaGroupDescription : INotifyPropertyChanged + { + public AvaloniaList GroupKeys { get; } + + public AvaloniaGroupDescription() + { + GroupKeys = new AvaloniaList(); + GroupKeys.CollectionChanged += (sender, e) => OnPropertyChanged(new PropertyChangedEventArgs(nameof(GroupKeys))); + } + + protected virtual event PropertyChangedEventHandler PropertyChanged; + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add + { + PropertyChanged += value; + } + + remove + { + PropertyChanged -= value; + } + } + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + public virtual string PropertyName => String.Empty; + public abstract object GroupKeyFromItem(object item, int level, CultureInfo culture); + public virtual bool KeysMatch(object groupKey, object itemKey) + { + return object.Equals(groupKey, itemKey); + } + } + public class PathGroupDescription : AvaloniaGroupDescription + { + private string _propertyPath; + private Type _propertyType; + private IValueConverter _valueConverter; + private StringComparison _stringComparison = StringComparison.Ordinal; + + public PathGroupDescription(string propertyPath) + { + _propertyPath = propertyPath; + } + + public override object GroupKeyFromItem(object item, int level, CultureInfo culture) + { + object GetKey(object o) + { + if(o == null) + return null; + + if (_propertyType == null) + _propertyType = GetPropertyType(o); + + return InvokePath(o, _propertyPath, _propertyType); + } + + var key = GetKey(item); + if (key == null) + key = item; + + if (_valueConverter != null) + key = _valueConverter.Convert(key, typeof(object), level, culture); + + return key; + } + public override bool KeysMatch(object groupKey, object itemKey) + { + if(groupKey is string k1 && itemKey is string k2) + { + return String.Equals(k1, k2, _stringComparison); + } + else + return base.KeysMatch(groupKey, itemKey); + } + public override string PropertyName => _propertyPath; + + private Type GetPropertyType(object o) + { + return o.GetType().GetNestedPropertyType(_propertyPath); + } + private static object InvokePath(object item, string propertyPath, Type propertyType) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); + if (exception != null) + { + throw exception; + } + return propertyValue; + } + } + + public abstract class CollectionViewGroup : INotifyPropertyChanged + { + private int _itemCount; + + public object Key { get; } + public int ItemCount => _itemCount; + public IAvaloniaReadOnlyList Items => ProtectedItems; + + protected AvaloniaList ProtectedItems { get; } + protected int ProtectedItemCount + { + get { return _itemCount; } + set + { + _itemCount = value; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(ItemCount))); + } + } + + protected CollectionViewGroup(object key) + { + Key = key; + ProtectedItems = new AvaloniaList(); + } + + public abstract bool IsBottomLevel { get; } + + protected virtual event PropertyChangedEventHandler PropertyChanged; + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add + { + PropertyChanged += value; + } + + remove + { + PropertyChanged -= value; + } + } + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + } + internal class CollectionViewGroupInternal : CollectionViewGroup + { + /// + /// GroupDescription used to define how to group the items + /// + private AvaloniaGroupDescription _groupBy; + + /// + /// Parent group of this CollectionViewGroupInternal + /// + private readonly CollectionViewGroupInternal _parentGroup; + + /// + /// Used for detecting stale enumerators + /// + private int _version; + + public CollectionViewGroupInternal(object key, CollectionViewGroupInternal parent) + : base(key) + { + _parentGroup = parent; + } + + public override bool IsBottomLevel => _groupBy == null; + + internal int FullCount { get; set; } + + internal AvaloniaGroupDescription GroupBy + { + get { return _groupBy; } + set + { + bool oldIsBottomLevel = IsBottomLevel; + + if (_groupBy != null) + { + ((INotifyPropertyChanged)_groupBy).PropertyChanged -= OnGroupByChanged; + } + + _groupBy = value; + + if (_groupBy != null) + { + ((INotifyPropertyChanged)_groupBy).PropertyChanged += OnGroupByChanged; + } + + if (oldIsBottomLevel != IsBottomLevel) + { + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBottomLevel))); + } + } + } + + private void OnGroupByChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + OnGroupByChanged(); + } + protected virtual void OnGroupByChanged() + { + _parentGroup?.OnGroupByChanged(); + } + + + /// + /// Gets or sets the most recent index where activity took place + /// + internal int LastIndex { get; set; } + + /// + /// Gets the first item (leaf) added to this group. If this can't be determined, + /// DependencyProperty.UnsetValue. + /// + internal object SeedItem + { + get + { + if (ItemCount > 0 && (GroupBy == null || GroupBy.GroupKeys.Count == 0)) + { + // look for first item, child by child + for (int k = 0, n = Items.Count; k < n; ++k) + { + if (!(Items[k] is CollectionViewGroupInternal subgroup)) + { + // child is an item - return it + return Items[k]; + } + else if (subgroup.ItemCount > 0) + { + // child is a nonempty subgroup - ask it + return subgroup.SeedItem; + } + //// otherwise child is an empty subgroup - go to next child + } + + // we shouldn't get here, but just in case... + + return AvaloniaProperty.UnsetValue; + } + else + { + // the group is empty, or it has explicit subgroups. + // In either case, we cannot determine the first item - + // it could have gone into any of the subgroups. + return AvaloniaProperty.UnsetValue; + } + } + } + + private CollectionViewGroupInternal Parent => _parentGroup; + + /// + /// Adds the specified item to the collection + /// + /// Item to add + internal void Add(object item) + { + ChangeCounts(item, +1); + ProtectedItems.Add(item); + } + + /// + /// Clears the collection of items + /// + internal void Clear() + { + ProtectedItems.Clear(); + FullCount = 1; + ProtectedItemCount = 0; + } + + /// + /// Finds the index of the specified item + /// + /// Item we are looking for + /// Seed of the item we are looking for + /// Comparer used to find the item + /// Low range of item index + /// High range of item index + /// Index of the specified item + protected virtual int FindIndex(object item, object seed, IComparer comparer, int low, int high) + { + int index; + + if (comparer != null) + { + if (comparer is ListComparer listComparer) + { + // reset the IListComparer before each search. This cannot be done + // any less frequently (e.g. in Root.AddToSubgroups), due to the + // possibility that the item may appear in more than one subgroup. + listComparer.Reset(); + } + + if (comparer is CollectionViewGroupComparer groupComparer) + { + // reset the CollectionViewGroupComparer before each search. This cannot be done + // any less frequently (e.g. in Root.AddToSubgroups), due to the + // possibility that the item may appear in more than one subgroup. + groupComparer.Reset(); + } + + for (index = low; index < high; ++index) + { + object seed1 = (ProtectedItems[index] is CollectionViewGroupInternal subgroup) ? subgroup.SeedItem : ProtectedItems[index]; + if (seed1 == AvaloniaProperty.UnsetValue) + { + continue; + } + if (comparer.Compare(seed, seed1) < 0) + { + break; + } + } + } + else + { + index = high; + } + + return index; + } + + /// + /// Returns an enumerator over the leaves governed by this group + /// + /// Enumerator of leaves + internal IEnumerator GetLeafEnumerator() + { + return new LeafEnumerator(this); + } + + /// + /// Insert a new item or subgroup and return its index. Seed is a + /// representative from the subgroup (or the item itself) that + /// is used to position the new item/subgroup w.r.t. the order given + /// by the comparer. (If comparer is null, just add at the end). + /// + /// Item we are looking for + /// Seed of the item we are looking for + /// Comparer used to find the item + /// The index where the item was inserted + internal int Insert(object item, object seed, IComparer comparer) + { + // never insert the new item/group before the explicit subgroups + int low = (GroupBy == null) ? 0 : GroupBy.GroupKeys.Count; + int index = FindIndex(item, seed, comparer, low, ProtectedItems.Count); + + // now insert the item + ChangeCounts(item, +1); + ProtectedItems.Insert(index, item); + + return index; + } + + /// + /// Return the item at the given index within the list of leaves governed + /// by this group + /// + /// Index of the leaf + /// Item at given index + internal object LeafAt(int index) + { + for (int k = 0, n = Items.Count; k < n; ++k) + { + if (Items[k] is CollectionViewGroupInternal subgroup) + { + // current item is a group - either drill in, or skip over + if (index < subgroup.ItemCount) + { + return subgroup.LeafAt(index); + } + else + { + index -= subgroup.ItemCount; + } + } + else + { + // current item is a leaf - see if we're done + if (index == 0) + { + return Items[k]; + } + else + { + index -= 1; + } + } + } + + return null; + } + + /// + /// Returns the index of the given item within the list of leaves governed + /// by the full group structure. The item must be a (direct) child of this + /// group. The caller provides the index of the item within this group, + /// if known, or -1 if not. + /// + /// Item we are looking for + /// Index of the leaf + /// Number of items under that leaf + internal int LeafIndexFromItem(object item, int index) + { + int result = 0; + + // accumulate the number of predecessors at each level + for (CollectionViewGroupInternal group = this; + group != null; + item = group, group = group.Parent, index = -1) + { + // accumulate the number of predecessors at the level of item + for (int k = 0, n = group.Items.Count; k < n; ++k) + { + // if we've reached the item, move up to the next level + if ((index < 0 && Object.Equals(item, group.Items[k])) || + index == k) + { + break; + } + + // accumulate leaf count + CollectionViewGroupInternal subgroup = group.Items[k] as CollectionViewGroupInternal; + result += subgroup?.ItemCount ?? 1; + } + } + + return result; + } + + /// + /// Returns the index of the given item within the list of leaves governed + /// by this group + /// + /// Item we are looking for + /// Number of items under that leaf + internal int LeafIndexOf(object item) + { + int leaves = 0; // number of leaves we've passed over so far + for (int k = 0, n = Items.Count; k < n; ++k) + { + if (Items[k] is CollectionViewGroupInternal subgroup) + { + int subgroupIndex = subgroup.LeafIndexOf(item); + if (subgroupIndex < 0) + { + leaves += subgroup.ItemCount; // item not in this subgroup + } + else + { + return leaves + subgroupIndex; // item is in this subgroup + } + } + else + { + // current item is a leaf - compare it directly + if (Object.Equals(item, Items[k])) + { + return leaves; + } + else + { + leaves += 1; + } + } + } + + // item not found + return -1; + } + + /// + /// Removes the specified item from the collection + /// + /// Item to remove + /// Whether we want to return the leaf index + /// Leaf index where item was removed, if value was specified. Otherwise '-1' + internal int Remove(object item, bool returnLeafIndex) + { + int index = -1; + int localIndex = ProtectedItems.IndexOf(item); + + if (localIndex >= 0) + { + if (returnLeafIndex) + { + index = LeafIndexFromItem(null, localIndex); + } + + ChangeCounts(item, -1); + ProtectedItems.RemoveAt(localIndex); + } + + return index; + } + + /// + /// Removes an empty group from the PagedCollectionView grouping + /// + /// Empty subgroup to remove + private static void RemoveEmptyGroup(CollectionViewGroupInternal group) + { + CollectionViewGroupInternal parent = group.Parent; + + if (parent != null) + { + AvaloniaGroupDescription groupBy = parent.GroupBy; + int index = parent.ProtectedItems.IndexOf(group); + + // remove the subgroup unless it is one of the explicit groups + if (index >= groupBy.GroupKeys.Count) + { + parent.Remove(group, false); + } + } + } + + + /// + /// Update the item count of the CollectionViewGroup + /// + /// CollectionViewGroup to update + /// Delta to change count by + protected void ChangeCounts(object item, int delta) + { + bool changeLeafCount = !(item is CollectionViewGroup); + + for (CollectionViewGroupInternal group = this; + group != null; + group = group._parentGroup) + { + group.FullCount += delta; + if (changeLeafCount) + { + group.ProtectedItemCount += delta; + + if (group.ProtectedItemCount == 0) + { + RemoveEmptyGroup(group); + } + } + } + + unchecked + { + // this invalidates enumerators + ++_version; + } + } + + + /// + /// Enumerator for the leaves in the CollectionViewGroupInternal class. + /// + private class LeafEnumerator : IEnumerator + { + private object _current; // current item + private CollectionViewGroupInternal _group; // parent group + private int _index; // current index into Items + private IEnumerator _subEnum; // enumerator over current subgroup + private int _version; // parent group's version at ctor + + /// + /// Initializes a new instance of the LeafEnumerator class. + /// + /// CollectionViewGroupInternal that uses the enumerator + public LeafEnumerator(CollectionViewGroupInternal group) + { + _group = group; + DoReset(); // don't call virtual Reset in ctor + } + + /// + /// Private helper to reset the enumerator + /// + private void DoReset() + { + Debug.Assert(_group != null, "_group should have been initialized in constructor"); + _version = _group._version; + _index = -1; + _subEnum = null; + } + + #region Implement IEnumerator + + /// + /// Reset implementation for IEnumerator + /// + void IEnumerator.Reset() + { + DoReset(); + } + + /// + /// MoveNext implementation for IEnumerator + /// + /// Returns whether the MoveNext operation was successful + bool IEnumerator.MoveNext() + { + Debug.Assert(_group != null, "_group should have been initialized in constructor"); + + // check for invalidated enumerator + if (_group._version != _version) + { + throw new InvalidOperationException(); + } + + // move forward to the next leaf + while (_subEnum == null || !_subEnum.MoveNext()) + { + // done with the current top-level item. Move to the next one. + ++_index; + if (_index >= _group.Items.Count) + { + return false; + } + + CollectionViewGroupInternal subgroup = _group.Items[_index] as CollectionViewGroupInternal; + if (subgroup == null) + { + // current item is a leaf - it's the new Current + _current = _group.Items[_index]; + _subEnum = null; + return true; + } + else + { + // current item is a subgroup - get its enumerator + _subEnum = subgroup.GetLeafEnumerator(); + } + } + + // the loop terminates only when we have a subgroup enumerator + // positioned at the new Current item + _current = _subEnum.Current; + return true; + } + + /// + /// Gets the current implementation for IEnumerator + /// + object IEnumerator.Current + { + get + { + Debug.Assert(_group != null, "_group should have been initialized in constructor"); + + if (_index < 0 || _index >= _group.Items.Count) + { + throw new InvalidOperationException(); + } + + return _current; + } + } + + #endregion Implement IEnumerator + } + + // / + // / This comparer is used to insert an item into a group in a position consistent + // / with a given IList. It only works when used in the pattern that FindIndex + // / uses, namely first call Reset(), then call Compare(item, itemSequence) any number of + // / times with the same item (the new item) as the first argument, and a sequence + // / of items as the second argument that appear in the IList in the same sequence. + // / This makes the total search time linear in the size of the IList. (To give + // / the correct answer regardless of the sequence of arguments would involve + // / calling IndexOf and leads to O(N^2) total search time.) + // / + internal class ListComparer : IComparer + { + /// + /// Constructor for the ListComparer that takes + /// in an IList. + /// + /// IList used to compare on + internal ListComparer(IList list) + { + ResetList(list); + } + + /// + /// Sets the index that we start comparing + /// from to 0. + /// + internal void Reset() + { + _index = 0; + } + + /// + /// Sets our IList to a new instance + /// of a list being passed in and resets + /// the index. + /// + /// IList used to compare on + internal void ResetList(IList list) + { + _list = list; + _index = 0; + } + + /// + /// Compares objects x and y to see which one + /// should appear first. + /// + /// The first object + /// The second object + /// -1 if x is less than y, +1 otherwise + public int Compare(object x, object y) + { + if (Object.Equals(x, y)) + { + return 0; + } + + // advance the index until seeing one x or y + int n = (_list != null) ? _list.Count : 0; + for (; _index < n; ++_index) + { + object z = _list[_index]; + if (Object.Equals(x, z)) + { + return -1; // x occurs first, so x < y + } + else if (Object.Equals(y, z)) + { + return +1; // y occurs first, so x > y + } + } + + // if we don't see either x or y, declare x > y. + // This has the effect of putting x at the end of the list. + return +1; + } + + private int _index; + private IList _list; + } + + // / + // / This comparer is used to insert an item into a group in a position consistent + // / with a given CollectionViewGroupRoot. We will only use this when dealing with + // / a temporary CollectionViewGroupRoot that points to the correct grouping of the + // / entire collection, and we have paging that requires us to keep the paged group + // / consistent with the order of items in the temporary group. + // / + internal class CollectionViewGroupComparer : IComparer + { + /// + /// Constructor for the CollectionViewGroupComparer that takes + /// in an CollectionViewGroupRoot. + /// + /// CollectionViewGroupRoot used to compare on + internal CollectionViewGroupComparer(CollectionViewGroupRoot group) + { + ResetGroup(group); + } + + /// + /// Sets the index that we start comparing + /// from to 0. + /// + internal void Reset() + { + _index = 0; + } + + /// + /// Sets our group to a new instance of a + /// CollectionViewGroupRoot being passed in + /// and resets the index. + /// + /// CollectionViewGroupRoot used to compare on + internal void ResetGroup(CollectionViewGroupRoot group) + { + _group = group; + _index = 0; + } + + /// + /// Compares objects x and y to see which one + /// should appear first. + /// + /// The first object + /// The second object + /// -1 if x is less than y, +1 otherwise + public int Compare(object x, object y) + { + if (Object.Equals(x, y)) + { + return 0; + } + + // advance the index until seeing one x or y + int n = (_group != null) ? _group.ItemCount : 0; + for (; _index < n; ++_index) + { + object z = _group.LeafAt(_index); + if (Object.Equals(x, z)) + { + return -1; // x occurs first, so x < y + } + else if (Object.Equals(y, z)) + { + return +1; // y occurs first, so x > y + } + } + + // if we don't see either x or y, declare x > y. + // This has the effect of putting x at the end of the list. + return +1; + } + + private int _index; + private CollectionViewGroupRoot _group; + } + + } + + internal class CollectionViewGroupRoot : CollectionViewGroupInternal, INotifyCollectionChanged + { + /// + /// String constant used for the Root Name + /// + private const string RootName = "Root"; + + /// + /// Private accessor for empty object instance + /// + private static readonly object UseAsItemDirectly = new object(); + + /// + /// Private accessor for the top level GroupDescription + /// + private static AvaloniaGroupDescription topLevelGroupDescription; + + /// + /// Private accessor for an ObservableCollection containing group descriptions + /// + private readonly AvaloniaList _groupBy = new AvaloniaList(); + + /// + /// Indicates whether the list of items (after applying the sort and filters, if any) + /// is already in the correct order for grouping. + /// + private bool _isDataInGroupOrder; + + /// + /// Private accessor for the owning ICollectionView + /// + private readonly ICollectionView _view; + + /// + /// Raise this event when the (grouped) view changes + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + /// Raise this event when the GroupDescriptions change + /// + internal event EventHandler GroupDescriptionChanged; + + /// + /// Initializes a new instance of the CollectionViewGroupRoot class. + /// + /// CollectionView that contains this grouping + /// True if items are already in correct order for grouping + internal CollectionViewGroupRoot(ICollectionView view, bool isDataInGroupOrder) + : base(RootName, null) + { + _view = view; + _isDataInGroupOrder = isDataInGroupOrder; + } + + /// + /// Gets the description of grouping, indexed by level. + /// + public virtual AvaloniaList GroupDescriptions => _groupBy; + + /// + /// Gets or sets the current IComparer being used + /// + internal IComparer ActiveComparer { get; set; } + + /// + /// Gets the culture to use during sorting. + /// + internal CultureInfo Culture + { + get + { + Debug.Assert(_view != null, "this._view should have been set from the constructor"); + return _view.Culture; + } + } + + /// + /// Gets or sets a value indicating whether the data is in group order + /// + internal bool IsDataInGroupOrder + { + get { return _isDataInGroupOrder; } + set { _isDataInGroupOrder = value; } + } + + + /// + /// Finds the index of the specified item + /// + /// Item we are looking for + /// Seed of the item we are looking for + /// Comparer used to find the item + /// Low range of item index + /// High range of item index + /// Index of the specified item + protected override int FindIndex(object item, object seed, IComparer comparer, int low, int high) + { + // root group needs to adjust the bounds of the search to exclude the new item (if any) + if (_view is IEditableCollectionView iecv && iecv.IsAddingNew) + { + --high; + } + + return base.FindIndex(item, seed, comparer, low, high); + } + + /// + /// Initializes the group descriptions + /// + internal void Initialize() + { + if (topLevelGroupDescription == null) + { + topLevelGroupDescription = new TopLevelGroupDescription(); + } + + InitializeGroup(this, 0, null); + } + + /// + /// Inserts specified item into the collection + /// + /// Index to insert into + /// Item to insert + /// Whether we are currently loading + internal void InsertSpecialItem(int index, object item, bool loading) + { + ChangeCounts(item, +1); + ProtectedItems.Insert(index, item); + + if (!loading) + { + int globalIndex = LeafIndexFromItem(item, index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, globalIndex)); + } + } + + /// + /// Notify listeners that this View has changed + /// + /// + /// CollectionViews (and sub-classes) should take their filter/sort/grouping + /// into account before calling this method to forward CollectionChanged events. + /// + /// The NotifyCollectionChangedEventArgs to be passed to the EventHandler + public void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + Debug.Assert(args != null, "Arguments passed in should not be null"); + CollectionChanged?.Invoke(this, args); + } + + /// + /// Notify host that a group description has changed somewhere in the tree + /// + protected override void OnGroupByChanged() + { + GroupDescriptionChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Remove specified item from subgroups + /// + /// Item to remove + /// Whether the operation was successful + internal bool RemoveFromSubgroups(object item) + { + return RemoveFromSubgroups(item, this, 0); + } + + /// + /// Remove specified item from subgroups using an exhaustive search + /// + /// Item to remove + internal void RemoveItemFromSubgroupsByExhaustiveSearch(object item) + { + RemoveItemFromSubgroupsByExhaustiveSearch(this, item); + } + + /// + /// Removes specified item into the collection + /// + /// Index to remove from + /// Item to remove + /// Whether we are currently loading + internal void RemoveSpecialItem(int index, object item, bool loading) + { + Debug.Assert(Object.Equals(item, ProtectedItems[index]), "RemoveSpecialItem finds inconsistent data"); + int globalIndex = -1; + + if (!loading) + { + globalIndex = LeafIndexFromItem(item, index); + } + + ChangeCounts(item, -1); + ProtectedItems.RemoveAt(index); + + if (!loading) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, globalIndex)); + } + } + + /// + /// Adds specified item to subgroups + /// + /// Item to add + /// Whether we are currently loading + internal void AddToSubgroups(object item, bool loading) + { + AddToSubgroups(item, this, 0, loading); + } + + /// + /// Add an item to the subgroup with the given name + /// + /// Item to add + /// Group to add item to + /// The level of grouping. + /// Name of subgroup to add to + /// Whether we are currently loading + private void AddToSubgroup(object item, CollectionViewGroupInternal group, int level, object key, bool loading) + { + CollectionViewGroupInternal subgroup; + int index = (_isDataInGroupOrder) ? group.LastIndex : 0; + + // find the desired subgroup + for (int n = group.Items.Count; index < n; ++index) + { + subgroup = group.Items[index] as CollectionViewGroupInternal; + if (subgroup == null) + { + continue; // skip children that are not groups + } + + if (group.GroupBy.KeysMatch(subgroup.Key, key)) + { + group.LastIndex = index; + AddToSubgroups(item, subgroup, level + 1, loading); + return; + } + } + + // the item didn't match any subgroups. Create a new subgroup and add the item. + subgroup = new CollectionViewGroupInternal(key, group); + InitializeGroup(subgroup, level + 1, item); + + + if (loading) + { + group.Add(subgroup); + group.LastIndex = index; + } + else + { + // using insert will find the correct sort index to + // place the subgroup, and will default to the last + // position if no ActiveComparer is specified + group.Insert(subgroup, item, ActiveComparer); + } + + AddToSubgroups(item, subgroup, level + 1, loading); + } + + + /// + /// Add an item to the desired subgroup(s) of the given group + /// + /// Item to add + /// Group to add item to + /// The level of grouping + /// Whether we are currently loading + private void AddToSubgroups(object item, CollectionViewGroupInternal group, int level, bool loading) + { + object key = GetGroupKey(item, group.GroupBy, level); + + if (key == UseAsItemDirectly) + { + // the item belongs to the group itself (not to any subgroups) + if (loading) + { + group.Add(item); + } + else + { + int localIndex = group.Insert(item, item, ActiveComparer); + int index = group.LeafIndexFromItem(item, localIndex); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + } + } + else if(key is ICollection keyList) + { + // the item belongs to multiple subgroups + foreach (object o in keyList) + { + AddToSubgroup(item, group, level, o, loading); + } + } + else + { + // the item belongs to one subgroup + AddToSubgroup(item, group, level, key, loading); + } + } + + public virtual Func GroupBySelector { get; set; } + + /// + /// Returns the description of how to divide the given group into subgroups + /// + /// CollectionViewGroup to get group description from + /// The level of grouping + /// GroupDescription of how to divide the given group + private AvaloniaGroupDescription GetGroupDescription(CollectionViewGroup group, int level) + { + AvaloniaGroupDescription result = null; + if (group == this) + { + group = null; + } + + if (result == null && GroupBySelector != null) + { + result = GroupBySelector?.Invoke(group, level); + } + + if (result == null && level < GroupDescriptions.Count) + { + result = GroupDescriptions[level]; + } + + return result; + } + + /// + /// Get the group name(s) for the given item + /// + /// Item to get group name for + /// GroupDescription for the group + /// The level of grouping + /// Group names for the specified item + private object GetGroupKey(object item, AvaloniaGroupDescription groupDescription, int level) + { + if (groupDescription != null) + { + return groupDescription.GroupKeyFromItem(item, level, Culture); + } + else + { + return UseAsItemDirectly; + } + } + + /// + /// Initialize the given group + /// + /// Group to initialize + /// The level of grouping + /// The seed item to compare with to see where to insert + private void InitializeGroup(CollectionViewGroupInternal group, int level, object seedItem) + { + // set the group description for dividing the group into subgroups + AvaloniaGroupDescription groupDescription = GetGroupDescription(group, level); + group.GroupBy = groupDescription; + + // create subgroups for each of the explicit names + var keys = groupDescription?.GroupKeys; + if (keys != null) + { + for (int k = 0, n = keys.Count; k < n; ++k) + { + CollectionViewGroupInternal subgroup = new CollectionViewGroupInternal(keys[k], group); + InitializeGroup(subgroup, level + 1, seedItem); + group.Add(subgroup); + } + } + + group.LastIndex = 0; + } + + /// + /// Remove an item from the direct children of a group. + /// + /// Group to remove item from + /// Item to remove + /// True if item could not be removed + private bool RemoveFromGroupDirectly(CollectionViewGroupInternal group, object item) + { + int leafIndex = group.Remove(item, true); + if (leafIndex >= 0) + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, leafIndex)); + return false; + } + else + { + return true; + } + } + + /// + /// Remove an item from the subgroup with the given name. + /// + /// Item to remove + /// Group to remove item from + /// The level of grouping + /// Name of item to remove + /// Return true if the item was not in one of the subgroups it was supposed to be. + private bool RemoveFromSubgroup(object item, CollectionViewGroupInternal group, int level, object key) + { + bool itemIsMissing = false; + CollectionViewGroupInternal subgroup; + + // find the desired subgroup + for (int index = 0, n = group.Items.Count; index < n; ++index) + { + subgroup = group.Items[index] as CollectionViewGroupInternal; + if (subgroup == null) + { + continue; // skip children that are not groups + } + + if (group.GroupBy.KeysMatch(subgroup.Key, key)) + { + if (RemoveFromSubgroups(item, subgroup, level + 1)) + { + itemIsMissing = true; + } + + return itemIsMissing; + } + } + + // the item didn't match any subgroups. It should have. + return true; + } + + /// + /// Remove an item from the desired subgroup(s) of the given group. + /// + /// Item to remove + /// Group to remove item from + /// The level of grouping + /// Return true if the item was not in one of the subgroups it was supposed to be. + private bool RemoveFromSubgroups(object item, CollectionViewGroupInternal group, int level) + { + bool itemIsMissing = false; + object key = GetGroupKey(item, group.GroupBy, level); + + if (key == UseAsItemDirectly) + { + // the item belongs to the group itself (not to any subgroups) + itemIsMissing = RemoveFromGroupDirectly(group, item); + } + else if (key is ICollection keyList) + { + // the item belongs to multiple subgroups + foreach (object o in keyList) + { + if (RemoveFromSubgroup(item, group, level, o)) + { + itemIsMissing = true; + } + } + } + else + { + // the item belongs to one subgroup + if (RemoveFromSubgroup(item, group, level, key)) + { + itemIsMissing = true; + } + } + + return itemIsMissing; + } + + /// + /// The item did not appear in one or more of the subgroups it + /// was supposed to. This can happen if the item's properties + /// change so that the group names we used to insert it are + /// different from the names used to remove it. If this happens, + /// remove the item the hard way. + /// + /// Group to remove item from + /// Item to remove + private void RemoveItemFromSubgroupsByExhaustiveSearch(CollectionViewGroupInternal group, object item) + { + // try to remove the item from the direct children + // this function only returns true if it failed to remove from group directly + // in which case we will step through and search exhaustively + if (RemoveFromGroupDirectly(group, item)) + { + // if that didn't work, recurse into each subgroup + // (loop runs backwards in case an entire group is deleted) + for (int k = group.Items.Count - 1; k >= 0; --k) + { + if (group.Items[k] is CollectionViewGroupInternal subgroup) + { + RemoveItemFromSubgroupsByExhaustiveSearch(subgroup, item); + } + } + } + } + + + /// + /// TopLevelGroupDescription class + /// + private class TopLevelGroupDescription : AvaloniaGroupDescription + { + /// + /// Initializes a new instance of the TopLevelGroupDescription class. + /// + public TopLevelGroupDescription() + { + } + + /// + /// We have to implement this abstract method, but it should never be called + /// + /// Item to get group name from + /// The level of grouping + /// Culture used for sorting + /// We do not return a value here + public override object GroupKeyFromItem(object item, int level, CultureInfo culture) + { + Debug.Assert(true, "We have to implement this abstract method, but it should never be called"); + return null; + } + } + } + +} diff --git a/src/Avalonia.DataGrid/Collections/AvaloniaSortDescription.cs b/src/Avalonia.DataGrid/Collections/AvaloniaSortDescription.cs new file mode 100644 index 0000000000..6ecb4f9a20 --- /dev/null +++ b/src/Avalonia.DataGrid/Collections/AvaloniaSortDescription.cs @@ -0,0 +1,261 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Avalonia.Controls; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public abstract class AvaloniaSortDescription + { + public virtual string PropertyPath => null; + public virtual bool Descending => false; + public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); + public abstract IComparer Comparer { get; } + + public virtual IOrderedEnumerable OrderBy(IEnumerable seq) + { + return seq.OrderBy(o => o, Comparer); + } + public virtual IOrderedEnumerable ThenBy(IOrderedEnumerable seq) + { + return seq.ThenBy(o => o, Comparer); + } + + internal virtual AvaloniaSortDescription SwitchSortDirection() + { + return this; + } + + internal virtual void Initialize(Type itemType) + { } + + private static object InvokePath(object item, string propertyPath, Type propertyType) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); + if (exception != null) + { + throw exception; + } + return propertyValue; + } + + /// + /// Creates a comparer class that takes in a CultureInfo as a parameter, + /// which it will use when comparing strings. + /// + private class CultureSensitiveComparer : Comparer + { + /// + /// Private accessor for the CultureInfo of our comparer + /// + private CultureInfo _culture; + + /// + /// Creates a comparer which will respect the CultureInfo + /// that is passed in when comparing strings. + /// + /// The CultureInfo to use in string comparisons + public CultureSensitiveComparer(CultureInfo culture) + : base() + { + _culture = culture ?? CultureInfo.InvariantCulture; + } + + #region IComparer Members + + /// + /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. + /// + /// first item to compare + /// second item to compare + /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y + /// + /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects. + /// + public override int Compare(object x, object y) + { + if (x == null) + { + if (y != null) + { + return -1; + } + return 0; + } + if (y == null) + { + return 1; + } + + // at this point x and y are not null + if (x.GetType() == typeof(string) && y.GetType() == typeof(string)) + { + return _culture.CompareInfo.Compare((string)x, (string)y); + } + else + { + return Comparer.Default.Compare(x, y); + } + } + + #endregion + } + + private class PathSortDescription : AvaloniaSortDescription + { + private readonly bool _descending; + private readonly string _propertyPath; + private readonly Lazy _cultureSensitiveComparer; + private readonly Lazy> _comparer; + private Type _propertyType; + private IComparer _internalComparer; + private IComparer _internalComparerTyped; + private IComparer InternalComparer + { + get + { + if (_internalComparerTyped == null && _internalComparer != null) + { + if (_internalComparerTyped is IComparer c) + _internalComparerTyped = c; + else + _internalComparerTyped = Comparer.Create((x, y) => _internalComparer.Compare(x, y)); + } + + return _internalComparerTyped; + } + } + + public override string PropertyPath => _propertyPath; + public override IComparer Comparer => _comparer.Value; + public override bool Descending => _descending; + + public PathSortDescription(string propertyPath, bool descending, CultureInfo culture) + { + _propertyPath = propertyPath; + _descending = descending; + _cultureSensitiveComparer = new Lazy(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); + _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); + } + private PathSortDescription(PathSortDescription inner, bool descending) + { + _propertyPath = inner._propertyPath; + _descending = descending; + _propertyType = inner._propertyType; + _cultureSensitiveComparer = inner._cultureSensitiveComparer; + _internalComparer = inner._internalComparer; + _internalComparerTyped = inner._internalComparerTyped; + + _comparer = new Lazy>(() => Comparer.Create((x, y) => Compare(x, y))); + } + + private object GetValue(object o) + { + if (o == null) + return null; + + if (HasPropertyPath) + return InvokePath(o, _propertyPath, _propertyType); + + if (_propertyType == o.GetType()) + return o; + else + return null; + } + + private IComparer GetComparerForType(Type type) + { + if (type == typeof(string)) + return _cultureSensitiveComparer.Value; + else + return (typeof(Comparer<>).MakeGenericType(type).GetProperty("Default")).GetValue(null, null) as IComparer; + } + private Type GetPropertyType(object o) + { + return o.GetType().GetNestedPropertyType(_propertyPath); + } + + private int Compare(object x, object y) + { + int result = 0; + + if(_propertyType == null) + { + if(x != null) + { + _propertyType = GetPropertyType(x); + } + if(_propertyType == null && y != null) + { + _propertyType = GetPropertyType(y); + } + } + + object v1 = GetValue(x); + object v2 = GetValue(y); + + if (_propertyType != null && _internalComparer == null) + _internalComparer = GetComparerForType(_propertyType); + + result = _internalComparer?.Compare(v1, v2) ?? 0; + + if (_descending) + return -result; + else + return result; + } + + internal override void Initialize(Type itemType) + { + base.Initialize(itemType); + + if(_propertyType == null) + _propertyType = itemType.GetNestedPropertyType(_propertyPath); + if (_internalComparer == null && _propertyType != null) + _internalComparer = GetComparerForType(_propertyType); + } + public override IOrderedEnumerable OrderBy(IEnumerable seq) + { + if(_descending) + { + return seq.OrderByDescending(o => GetValue(o), InternalComparer); + } + else + { + return seq.OrderBy(o => GetValue(o), InternalComparer); + } + } + public override IOrderedEnumerable ThenBy(IOrderedEnumerable seq) + { + if (_descending) + { + return seq.ThenByDescending(o => GetValue(o), InternalComparer); + } + else + { + return seq.ThenByDescending(o => GetValue(o), InternalComparer); + } + } + + internal override AvaloniaSortDescription SwitchSortDirection() + { + return new PathSortDescription(this, !_descending); + } + } + + public static AvaloniaSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) + { + return new PathSortDescription(propertyPath, descending, culture); + } + } + + public class AvaloniaSortDescriptionCollection : AvaloniaList + { } +} diff --git a/src/Avalonia.DataGrid/Collections/CollectionView.cs b/src/Avalonia.DataGrid/Collections/CollectionView.cs new file mode 100644 index 0000000000..0864f45514 --- /dev/null +++ b/src/Avalonia.DataGrid/Collections/CollectionView.cs @@ -0,0 +1,4470 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Avalonia.Collections +{ + /// + /// Event argument used for page index change notifications. The requested page move + /// can be canceled by setting e.Cancel to True. + /// + /// Preview + public sealed class PageChangingEventArgs : CancelEventArgs + { + /// + /// Constructor that takes the target page index + /// + /// Index of the requested page + public PageChangingEventArgs(int newPageIndex) + { + NewPageIndex = newPageIndex; + } + + /// + /// Gets the index of the requested page + /// + public int NewPageIndex + { + get; + private set; + } + } + + /// Defines a method that enables a collection to provide a custom view for specialized sorting, filtering, grouping, and currency. + public interface ICollectionViewFactory + { + /// Returns a custom view for specialized sorting, filtering, grouping, and currency. + /// A custom view for specialized sorting, filtering, grouping, and currency. + ICollectionView CreateView(); + } + + /// + /// PagedCollectionView view over an IEnumerable. + /// + public sealed class CollectionViewBase : ICollectionView, IEditableCollectionView, INotifyPropertyChanged //IPagedCollectionView, + { + #region Static Fields and Constants + + /// + /// Since there's nothing in the un-cancelable event args that is mutable, + /// just create one instance to be used universally. + /// + private static readonly CurrentChangingEventArgs uncancelableCurrentChangingEventArgs = new CurrentChangingEventArgs(false); + + #endregion Static Fields and Constants + + #region Private Fields + + /// + /// Value that we cache for the PageIndex if we are in a DeferRefresh, + /// and the user has attempted to move to a different page. + /// + private int _cachedPageIndex = -1; + + /// + /// Value that we cache for the PageSize if we are in a DeferRefresh, + /// and the user has attempted to change the PageSize. + /// + private int _cachedPageSize; + + /// + /// CultureInfo used in this PagedCollectionView + /// + private CultureInfo _culture; + + /// + /// Private accessor for the Monitor we use to prevent recursion + /// + private SimpleMonitor _currentChangedMonitor = new SimpleMonitor(); + + /// + /// Private accessor for the CurrentItem + /// + private object _currentItem; + + /// + /// Private accessor for the CurrentPosition + /// + private int _currentPosition; + + /// + /// The number of requests to defer Refresh() + /// + private int _deferLevel; + + /// + /// The item we are currently editing + /// + private object _editItem; + + /// + /// Private accessor for the Filter + /// + private Func _filter; + + /// + /// Private accessor for the CollectionViewFlags + /// + private CollectionViewFlags _flags = CollectionViewFlags.ShouldProcessCollectionChanged; + + /// + /// Private accessor for the Grouping data + /// + private CollectionViewGroupRoot _group; + + /// + /// Private accessor for the InternalList + /// + private IList _internalList; + + /// + /// Keeps track of whether groups have been applied to the + /// collection already or not. Note that this can still be set + /// to false even though we specify a GroupDescription, as the + /// collection may not have gone through the PrepareGroups function. + /// + private bool _isGrouping; + + /// + /// Private accessor for indicating whether we want to point to the temporary grouping data for calculations + /// + private bool _isUsingTemporaryGroup; + + /// + /// ConstructorInfo obtained from reflection for generating new items + /// + private ConstructorInfo _itemConstructor; + + /// + /// Whether we have the correct ConstructorInfo information for the ItemConstructor + /// + private bool _itemConstructorIsValid; + + /// + /// The new item we are getting ready to add to the collection + /// + private object _newItem; + + /// + /// Private accessor for the PageIndex + /// + private int _pageIndex = -1; + + /// + /// Private accessor for the PageSize + /// + private int _pageSize; + + /// + /// Whether the source needs to poll for changes + /// (if it did not implement INotifyCollectionChanged) + /// + private bool _pollForChanges; + + /// + /// Private accessor for the SortDescriptions + /// + private AvaloniaSortDescriptionCollection _sortDescriptions; + + /// + /// Private accessor for the SourceCollection + /// + private IEnumerable _sourceCollection; + + /// + /// Private accessor for the Grouping data on the entire collection + /// + private CollectionViewGroupRoot _temporaryGroup; + + /// + /// Timestamp used to see if there was a collection change while + /// processing enumerator changes + /// + private int _timestamp; + + /// + /// Private accessor for the TrackingEnumerator + /// + private IEnumerator _trackingEnumerator; + + #endregion Private Fields + + #region Constructors + + /// + /// Helper constructor that sets default values for isDataSorted and isDataInGroupOrder. + /// + /// The source for the collection + public CollectionViewBase(IEnumerable source) + : this(source, false /*isDataSorted*/, false /*isDataInGroupOrder*/) + { + } + + /// + /// Initializes a new instance of the PagedCollectionView class. + /// + /// The source for the collection + /// Determines whether the source is already sorted + /// Whether the source is already in the correct order for grouping + public CollectionViewBase(IEnumerable source, bool isDataSorted, bool isDataInGroupOrder) + { + _sourceCollection = source ?? throw new ArgumentNullException(nameof(source)); + + SetFlag(CollectionViewFlags.IsDataSorted, isDataSorted); + SetFlag(CollectionViewFlags.IsDataInGroupOrder, isDataInGroupOrder); + + _temporaryGroup = new CollectionViewGroupRoot(this, isDataInGroupOrder); + _group = new CollectionViewGroupRoot(this, false); + _group.GroupDescriptionChanged += OnGroupDescriptionChanged; + _group.GroupDescriptions.CollectionChanged += OnGroupByChanged; + + CopySourceToInternalList(); + _trackingEnumerator = source.GetEnumerator(); + + // set currency + if (_internalList.Count > 0) + { + SetCurrent(_internalList[0], 0, 1); + } + else + { + SetCurrent(null, -1, 0); + } + + // Set flag for whether the collection is empty + SetFlag(CollectionViewFlags.CachedIsEmpty, Count == 0); + + // If we implement INotifyCollectionChanged + if (source is INotifyCollectionChanged coll) + { + coll.CollectionChanged += (_, args) => ProcessCollectionChanged(args); + } + else + { + // If the source doesn't raise collection change events, try to + // detect changes by polling the enumerator + _pollForChanges = true; + } + } + + #endregion Constructors + + #region Private Delegates + + // / + // / Delegate used to asynchronously trigger a page move. + // / + // / Requested page index + //TODO Not Needed? + //private delegate void RequestPageMoveDelegate(int pageIndex); + + #endregion Private Delegates + + #region Events + + /// + /// Raise this event when the (filtered) view changes + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + /// CollectionChanged event (per INotifyCollectionChanged). + /// + event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged + { + add { CollectionChanged += value; } + remove { CollectionChanged -= value; } + } + + /// + /// Raised when the CurrentItem property changed + /// + public event EventHandler CurrentChanged; + + /// + /// Raised when the CurrentItem property is changing + /// + public event EventHandler CurrentChanging; + + /// + /// Raised when a page index change completed + /// + //TODO Paging + public event EventHandler PageChanged; + + /// + /// Raised when a page index change is requested + /// + //TODO Paging + public event EventHandler PageChanging; + + /// + /// PropertyChanged event. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// PropertyChanged event (per INotifyPropertyChanged) + /// + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add { PropertyChanged += value; } + remove { PropertyChanged -= value; } + } + + #endregion Events + + #region Enumerators + + /// + /// Enum for CollectionViewFlags + /// + //TODO Paging + [Flags] + private enum CollectionViewFlags + { + /// + /// Whether the list of items (after applying the sort and filters, if any) + /// is already in the correct order for grouping. + /// + IsDataInGroupOrder = 0x01, + + /// + /// Whether the source collection is already sorted according to the SortDescriptions collection + /// + IsDataSorted = 0x02, + + /// + /// Whether we should process the collection changed event + /// + ShouldProcessCollectionChanged = 0x04, + + /// + /// Whether the current item is before the first + /// + IsCurrentBeforeFirst = 0x08, + + /// + /// Whether the current item is after the last + /// + IsCurrentAfterLast = 0x10, + + /// + /// Whether we need to refresh + /// + NeedsRefresh = 0x20, + + /// + /// Whether we cache the IsEmpty value + /// + CachedIsEmpty = 0x40, + + /// + /// Indicates whether a page index change is in process or not + /// + IsPageChanging = 0x80, + + /// + /// Whether we need to move to another page after EndDefer + /// + IsMoveToPageDeferred = 0x100, + + /// + /// Whether we need to update the PageSize after EndDefer + /// + IsUpdatePageSizeDeferred = 0x200 + } + + #endregion Enumerators + + private Type _itemType; + private Type ItemType + { + get + { + if (_itemType == null) + _itemType = GetItemType(true); + + return _itemType; + } + } + + #region Public Properties + + /// + /// Gets a value indicating whether the view supports AddNew. + /// + public bool CanAddNew + { + get + { + return !IsEditingItem && + (SourceList != null && !SourceList.IsFixedSize && CanConstructItem); + } + } + + /// + /// Gets a value indicating whether the view supports the notion of "pending changes" + /// on the current edit item. This may vary, depending on the view and the particular + /// item. For example, a view might return true if the current edit item + /// implements IEditableObject, or if the view has special knowledge about + /// the item that it can use to support rollback of pending changes. + /// + public bool CanCancelEdit + { + get { return _editItem is IEditableObject; } + } + + /// + /// Gets a value indicating whether the PageIndex value is allowed to change or not. + /// + //TODO Paging + public bool CanChangePage + { + get { return true; } + } + + /// + /// Gets a value indicating whether we support filtering with this ICollectionView. + /// + public bool CanFilter + { + get { return true; } + } + + /// + /// Gets a value indicating whether this view supports grouping. + /// When this returns false, the rest of the interface is ignored. + /// + public bool CanGroup + { + get { return true; } + } + + /// + /// Gets a value indicating whether the view supports Remove and RemoveAt. + /// + public bool CanRemove + { + get + { + return !IsEditingItem && !IsAddingNew && + (SourceList != null && !SourceList.IsFixedSize); + } + } + + /// + /// Gets a value indicating whether we support sorting with this ICollectionView. + /// + public bool CanSort + { + get { return true; } + } + + /// + /// Gets the number of records in the view after + /// filtering, sorting, and paging. + /// + //TODO Paging + public int Count + { + get + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + + // if we have paging + if (PageSize > 0 && PageIndex > -1) + { + if (IsGrouping && !_isUsingTemporaryGroup) + { + return _group.ItemCount; + } + else + { + return Math.Max(0, Math.Min(PageSize, InternalCount - (_pageSize * PageIndex))); + } + } + else + { + if (IsGrouping) + { + if (_isUsingTemporaryGroup) + { + return _temporaryGroup.ItemCount; + } + else + { + return _group.ItemCount; + } + } + else + { + return InternalCount; + } + } + } + } + + /// + /// Gets or sets Culture to use during sorting. + /// + public CultureInfo Culture + { + get + { + return _culture; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (_culture != value) + { + _culture = value; + OnPropertyChanged(nameof(Culture)); + } + } + } + + /// + /// Gets the new item when an AddNew transaction is in progress + /// Otherwise it returns null. + /// + public object CurrentAddItem + { + get + { + return _newItem; + } + + private set + { + if (_newItem != value) + { + Debug.Assert(value == null || _newItem == null, "Old and new _newItem values are unexpectedly non null"); + _newItem = value; + OnPropertyChanged(nameof(IsAddingNew)); + OnPropertyChanged(nameof(CurrentAddItem)); + } + } + } + + /// + /// Gets the affected item when an EditItem transaction is in progress + /// Otherwise it returns null. + /// + public object CurrentEditItem + { + get + { + return _editItem; + } + + private set + { + if (_editItem != value) + { + Debug.Assert(value == null || _editItem == null, "Old and new _editItem values are unexpectedly non null"); + bool oldCanCancelEdit = CanCancelEdit; + _editItem = value; + OnPropertyChanged(nameof(IsEditingItem)); + OnPropertyChanged(nameof(CurrentEditItem)); + if (oldCanCancelEdit != CanCancelEdit) + { + OnPropertyChanged(nameof(CanCancelEdit)); + } + } + } + } + + /// + /// Gets the "current item" for this view + /// + public object CurrentItem + { + get + { + VerifyRefreshNotDeferred(); + return _currentItem; + } + } + + /// + /// Gets the ordinal position of the CurrentItem within the + /// (optionally sorted and filtered) view. + /// + public int CurrentPosition + { + get + { + VerifyRefreshNotDeferred(); + return _currentPosition; + } + } + + private string GetOperationNotAllowedDuringAddOrEditText(string action) + { + return $"'{action}' is not allowed during an AddNew or EditItem transaction."; + } + private string GetOperationNotAllowedText(string action, string transaction = null) + { + if (String.IsNullOrWhiteSpace(transaction)) + { + return $"'{action}' is not allowed for this view."; + } + else + { + return $"'{action}' is not allowed during a transaction started by '{transaction}'."; + } + } + + /// + /// Gets or sets the Filter, which is a callback set by the consumer of the ICollectionView + /// and used by the implementation of the ICollectionView to determine if an + /// item is suitable for inclusion in the view. + /// + /// + /// Simpler implementations do not support filtering and will throw a NotSupportedException. + /// Use property to test if filtering is supported before + /// assigning a non-null value. + /// + public Func Filter + { + get + { + return _filter; + } + + set + { + if (IsAddingNew || IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Filter))); + } + + if (!CanFilter) + { + throw new NotSupportedException("The Filter property cannot be set when the CanFilter property returns false."); + } + + if (_filter != value) + { + _filter = value; + RefreshOrDefer(); + OnPropertyChanged(nameof(Filter)); + } + } + } + + /// + /// Gets the description of grouping, indexed by level. + /// + public AvaloniaList GroupDescriptions + { + get + { + return _group?.GroupDescriptions; + } + } + + int ICollectionView.GroupingDepth => GroupDescriptions?.Count ?? 0; + string ICollectionView.GetGroupingPropertyNameAtDepth(int level) + { + var groups = GroupDescriptions; + if(groups != null && level >= 0 && level < groups.Count) + { + return groups[level].PropertyName; + } + else + { + return String.Empty; + } + } + + /// + /// Gets the top-level groups, constructed according to the descriptions + /// given in GroupDescriptions. + /// + public IAvaloniaReadOnlyList Groups + { + get + { + if (!IsGrouping) + { + return null; + } + + return RootGroup?.Items; + } + } + + /// + /// Gets a value indicating whether an "AddNew" transaction is in progress. + /// + public bool IsAddingNew + { + get { return _newItem != null; } + } + + /// + /// Gets a value indicating whether currency is beyond the end (End-Of-File). + /// + /// Whether IsCurrentAfterLast + public bool IsCurrentAfterLast + { + get + { + VerifyRefreshNotDeferred(); + return CheckFlag(CollectionViewFlags.IsCurrentAfterLast); + } + } + + /// + /// Gets a value indicating whether currency is before the beginning (Beginning-Of-File). + /// + /// Whether IsCurrentBeforeFirst + public bool IsCurrentBeforeFirst + { + get + { + VerifyRefreshNotDeferred(); + return CheckFlag(CollectionViewFlags.IsCurrentBeforeFirst); + } + } + + /// + /// Gets a value indicating whether an EditItem transaction is in progress. + /// + public bool IsEditingItem + { + get { return _editItem != null; } + } + + /// + /// Gets a value indicating whether the resulting (filtered) view is empty. + /// + public bool IsEmpty + { + get + { + EnsureCollectionInSync(); + return InternalCount == 0; + } + } + + /// + /// Gets a value indicating whether a page index change is in process or not. + /// + //TODO Paging + public bool IsPageChanging + { + get + { + return CheckFlag(CollectionViewFlags.IsPageChanging); + } + + private set + { + if (CheckFlag(CollectionViewFlags.IsPageChanging) != value) + { + SetFlag(CollectionViewFlags.IsPageChanging, value); + OnPropertyChanged(nameof(IsPageChanging)); + } + } + } + + /// + /// Gets the minimum number of items known to be in the source collection + /// that verify the current filter if any + /// + public int ItemCount + { + get + { + return InternalList.Count; + } + } + + /// + /// Gets a value indicating whether this view needs to be refreshed. + /// + public bool NeedsRefresh + { + get { return CheckFlag(CollectionViewFlags.NeedsRefresh); } + } + + + /// + /// Gets the current page we are on. (zero based) + /// + //TODO Paging + public int PageIndex + { + get + { + return _pageIndex; + } + } + + /// + /// Gets or sets the number of items to display on a page. If the + /// PageSize = 0, then we are not paging, and will display all items + /// in the collection. Otherwise, we will have separate pages for + /// the items to display. + /// + //TODO Paging + public int PageSize + { + get + { + return _pageSize; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("PageSize cannot have a negative value."); + } + + // if the Refresh is currently deferred, cache the desired PageSize + // and set the flag so that once the defer is over, we can then + // update the PageSize. + if (IsRefreshDeferred) + { + // set cached value and flag so that we update the PageSize on EndDefer + _cachedPageSize = value; + SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, true); + return; + } + + // to see whether or not to fire an OnPropertyChanged + int oldCount = Count; + + if (_pageSize != value) + { + // Remember current currency values for upcoming OnPropertyChanged notifications + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // Check if there is a current edited or new item so changes can be committed first. + if (CurrentAddItem != null || CurrentEditItem != null) + { + // Check with the ICollectionView.CurrentChanging listeners if it's OK to + // change the currency. If not, then we can't fire the event to allow them to + // commit their changes. So, we will not be able to change the PageSize. + if (!OkToChangeCurrent()) + { + throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); + } + + // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or + // cancelling an item that is no longer on the current page. That's acceptable and means that + // the potential _newItem or _editItem needs to be committed before this PageSize change. + // The reason why we temporarily reset currency here is to give a chance to the bound + // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit() + // for example as a result of changing currency. + SetCurrentToPosition(-1); + RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // If the bound controls did not successfully end their potential item editing/addition, we + // need to throw an exception to show that the PageSize change failed. + if (CurrentAddItem != null || CurrentEditItem != null) + { + throw new InvalidOperationException("Changing the PageSize is not allowed during an AddNew or EditItem transaction."); + } + } + + _pageSize = value; + OnPropertyChanged(nameof(PageSize)); + + if (_pageSize == 0) + { + // update the groups for the current page + //*************************************** + PrepareGroups(); + + // if we are not paging + MoveToPage(-1); + } + else if (_pageIndex != 0) + { + if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) + { + // if the temporaryGroup was not created yet and is out of sync + // then create it so that we can use it as a refernce while paging. + if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) + { + PrepareTemporaryGroups(); + } + + MoveToFirstPage(); + } + } + else if (IsGrouping) + { + // if the temporaryGroup was not created yet and is out of sync + // then create it so that we can use it as a refernce while paging. + if (_temporaryGroup.ItemCount != InternalList.Count) + { + // update the groups that get created for the + // entire collection as well as the current page + PrepareTemporaryGroups(); + } + + // update the groups for the current page + PrepareGroupsForCurrentPage(); + } + + // if the count has changed + if (Count != oldCount) + { + OnPropertyChanged(nameof(Count)); + } + + // reset currency values + ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // send a notification that our collection has been updated + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Reset)); + + // now raise currency changes at the end + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + } + } + + /// + /// Gets the Sort criteria to sort items in collection. + /// + /// + /// + /// Clear a sort criteria by assigning SortDescription.Empty to this property. + /// One or more sort criteria in form of + /// can be used, each specifying a property and direction to sort by. + /// + /// + /// + /// Simpler implementations do not support sorting and will throw a NotSupportedException. + /// Use property to test if sorting is supported before adding + /// to SortDescriptions. + /// + public AvaloniaSortDescriptionCollection SortDescriptions + { + get + { + if (_sortDescriptions == null) + { + SetSortDescriptions(new AvaloniaSortDescriptionCollection()); + } + + return _sortDescriptions; + } + } + + /// + /// Gets the source of the IEnumerable collection we are using for our view. + /// + public IEnumerable SourceCollection + { + get { return _sourceCollection; } + } + + /// + /// Gets the total number of items in the view before paging is applied. + /// + public int TotalItemCount + { + get + { + return InternalList.Count; + } + } + + #endregion Public Properties + + ////------------------------------------------------------ + //// + //// Private Properties + //// + ////------------------------------------------------------ + + #region Private Properties + + /// + /// Gets a value indicating whether we have a valid ItemConstructor of the correct type + /// + private bool CanConstructItem + { + get + { + if (!_itemConstructorIsValid) + { + EnsureItemConstructor(); + } + + return _itemConstructor != null; + } + } + + /// + /// Gets the private count without taking paging or + /// placeholders into account + /// + private int InternalCount + { + get { return InternalList.Count; } + } + + /// + /// Gets the InternalList + /// + private IList InternalList + { + get { return _internalList; } + } + + /// + /// Gets a value indicating whether CurrentItem and CurrentPosition are + /// up-to-date with the state and content of the collection. + /// + private bool IsCurrentInSync + { + get + { + if (IsCurrentInView) + { + return GetItemAt(CurrentPosition).Equals(CurrentItem); + } + else + { + return CurrentItem == null; + } + } + } + + /// + /// Gets a value indicating whether the current item is in the view + /// + private bool IsCurrentInView + { + get + { + VerifyRefreshNotDeferred(); + + // Calling IndexOf will check whether the specified currentItem + // is within the (paged) view. + return IndexOf(CurrentItem) >= 0; + } + } + + /// + /// Gets a value indicating whether or not we have grouping + /// taking place in this collection. + /// + private bool IsGrouping + { + get { return _isGrouping; } + } + + bool ICollectionView.IsGrouping => IsGrouping; + + /// + /// Gets a value indicating whether there + /// is still an outstanding DeferRefresh in + /// use. If at all possible, derived classes + /// should not call Refresh if IsRefreshDeferred + /// is true. + /// + private bool IsRefreshDeferred + { + get { return _deferLevel > 0; } + } + + /// + /// Gets whether the current page is empty and we need + /// to move to a previous page. + /// + //TODO Paging + private bool NeedToMoveToPreviousPage + { + get { return (PageSize > 0 && Count == 0 && PageIndex != 0 && PageCount == PageIndex); } + } + + /// + /// Gets a value indicating whether we are on the last local page + /// + //TODO Paging + private bool OnLastLocalPage + { + get + { + if (PageSize == 0) + { + return false; + } + + Debug.Assert(PageCount > 0, "Unexpected PageCount <= 0"); + + // if we have no items (PageCount==1) or there is just one page + if (PageCount == 1) + { + return true; + } + + return (PageIndex == PageCount - 1); + } + } + + /// + /// Gets the number of pages we currently have + /// + //TODO Paging + private int PageCount + { + get { return (_pageSize > 0) ? Math.Max(1, (int)Math.Ceiling((double)ItemCount / _pageSize)) : 0; } + } + + /// + /// Gets the root of the Group that we expose to the user + /// + private CollectionViewGroupRoot RootGroup + { + get + { + return _isUsingTemporaryGroup ? _temporaryGroup : _group; + } + } + + /// + /// Gets the SourceCollection as an IList + /// + private IList SourceList + { + get { return SourceCollection as IList; } + } + + /// + /// Gets Timestamp used by the NewItemAwareEnumerator to determine if a + /// collection change has occurred since the enumerator began. (If so, + /// MoveNext should throw.) + /// + private int Timestamp + { + get { return _timestamp; } + } + + /// + /// Gets a value indicating whether a private copy of the data + /// is needed for sorting, filtering, and paging. We want any deriving + /// classes to also be able to access this value to see whether or not + /// to use the default source collection, or the internal list. + /// + //TODO Paging + private bool UsesLocalArray + { + get { return SortDescriptions.Count > 0 || Filter != null || _pageSize > 0 || GroupDescriptions.Count > 0; } + } + + #endregion Private Properties + + ////------------------------------------------------------ + //// + //// Indexers + //// + ////------------------------------------------------------ + + #region Indexers + + /// + /// Return the item at the specified index + /// + /// Index of the item we want to retrieve + /// The item at the specified index + public object this[int index] + { + get { return GetItemAt(index); } + } + + #endregion Indexers + + ////------------------------------------------------------ + //// + //// Public Methods + //// + ////------------------------------------------------------ + + #region Public Methods + + /// + /// Add a new item to the underlying collection. Returns the new item. + /// After calling AddNew and changing the new item as desired, either + /// CommitNew or CancelNew" should be called to complete the transaction. + /// + /// The new item we are adding + //TODO Paging + public object AddNew() + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + + if (IsEditingItem) + { + // Implicitly close a previous EditItem + CommitEdit(); + } + + // Implicitly close a previous AddNew + CommitNew(); + + // Checking CanAddNew will validate that we have the correct itemConstructor + if (!CanAddNew) + { + throw new InvalidOperationException(GetOperationNotAllowedText(nameof(AddNew))); + } + + object newItem = null; + + if (_itemConstructor != null) + { + newItem = _itemConstructor.Invoke(null); + } + + try + { + // temporarily disable the CollectionChanged event + // handler so filtering, sorting, or grouping + // doesn't get applied yet + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); + + if (SourceList != null) + { + SourceList.Add(newItem); + } + } + finally + { + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); + } + + // Modify our _trackingEnumerator so that it shows that our collection is "up to date" + // and will not refresh for now. + _trackingEnumerator = _sourceCollection.GetEnumerator(); + + int addIndex; + int removeIndex = -1; + + // Adjust index based on where it should be displayed in view. + if (PageSize > 0) + { + // if the page is full (Count==PageSize), then replace last item (Count-1). + // otherwise, we just append at end (Count). + addIndex = Count - ((Count == PageSize) ? 1 : 0); + + // if the page is full, remove the last item to make space for the new one. + removeIndex = (Count == PageSize) ? addIndex : -1; + } + else + { + // for non-paged lists, we want to insert the item + // as the last item in the view + addIndex = Count; + } + + // if we need to remove an item from the view due to paging + if (removeIndex > -1) + { + object removeItem = GetItemAt(removeIndex); + if (IsGrouping) + { + _group.RemoveFromSubgroups(removeItem); + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + removeItem, + removeIndex)); + } + + // add the new item to the internal list + _internalList.Insert(ConvertToInternalIndex(addIndex), newItem); + OnPropertyChanged(nameof(ItemCount)); + + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForAdd(null, addIndex); + + if (IsGrouping) + { + _group.InsertSpecialItem(_group.Items.Count, newItem, false); + if (PageSize > 0) + { + _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, newItem, false); + } + } + + // fire collection changed. + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + newItem, + addIndex)); + + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // set the current new item + CurrentAddItem = newItem; + + MoveCurrentTo(newItem); + + // if the new item is editable, call BeginEdit on it + if (newItem is IEditableObject editableObject) + { + editableObject.BeginEdit(); + } + + return newItem; + } + + /// + /// Complete the transaction started by . + /// The pending changes (if any) to the item are discarded. + /// + public void CancelEdit() + { + if (IsAddingNew) + { + throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelEdit), nameof(AddNew))); + } + else if (!CanCancelEdit) + { + throw new InvalidOperationException("CancelEdit is not supported for the current edit item."); + } + + VerifyRefreshNotDeferred(); + + if (CurrentEditItem == null) + { + return; + } + + object editItem = CurrentEditItem; + CurrentEditItem = null; + + if (editItem is IEditableObject ieo) + { + ieo.CancelEdit(); + } + else + { + throw new InvalidOperationException("CancelEdit is not supported for the current edit item."); + } + } + + /// + /// Complete the transaction started by AddNew. The new + /// item is removed from the collection. + /// + //TODO Paging + public void CancelNew() + { + if (IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CancelNew), nameof(EditItem))); + } + + VerifyRefreshNotDeferred(); + + if (CurrentAddItem == null) + { + return; + } + + // get index of item before it is removed + int index = IndexOf(CurrentAddItem); + + // remove the new item from the underlying collection + try + { + // temporarily disable the CollectionChanged event + // handler so filtering, sorting, or grouping + // doesn't get applied yet + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); + + if (SourceList != null) + { + SourceList.Remove(CurrentAddItem); + } + } + finally + { + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); + } + + // Modify our _trackingEnumerator so that it shows that our collection is "up to date" + // and will not refresh for now. + _trackingEnumerator = _sourceCollection.GetEnumerator(); + + // fire the correct events + if (CurrentAddItem != null) + { + object newItem = EndAddNew(true); + + int addIndex = -1; + + // Adjust index based on where it should be displayed in view. + if (PageSize > 0 && !OnLastLocalPage) + { + // if there is paging and we are not on the last page, we need + // to bring in an item from the next page. + addIndex = Count - 1; + } + + // remove the new item from the internal list + InternalList.Remove(newItem); + + if (IsGrouping) + { + _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false); + if (PageSize > 0) + { + _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false); + } + } + + OnPropertyChanged(nameof(ItemCount)); + + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForRemove(index); + + // fire collection changed. + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + newItem, + index)); + + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // if we need to add an item into the view due to paging + if (addIndex > -1) + { + int internalIndex = ConvertToInternalIndex(addIndex); + object addItem = null; + if (IsGrouping) + { + addItem = _temporaryGroup.LeafAt(internalIndex); + _group.AddToSubgroups(addItem, loading: false); + } + else + { + addItem = InternalItemAt(internalIndex); + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + addItem, + IndexOf(addItem))); + } + } + } + + /// + /// Complete the transaction started by . + /// The pending changes (if any) to the item are committed. + /// + //TODO Paging + public void CommitEdit() + { + if (IsAddingNew) + { + throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitEdit), nameof(AddNew))); + } + + VerifyRefreshNotDeferred(); + + if (CurrentEditItem == null) + { + return; + } + + object editItem = CurrentEditItem; + CurrentEditItem = null; + + if (editItem is IEditableObject ieo) + { + ieo.EndEdit(); + } + + if (UsesLocalArray) + { + // first remove the item from the array so that we can insert into the correct position + int removeIndex = IndexOf(editItem); + int internalRemoveIndex = InternalIndexOf(editItem); + _internalList.Remove(editItem); + + // check whether to restore currency to the item being edited + object restoreCurrencyTo = (editItem == CurrentItem) ? editItem : null; + + if (removeIndex >= 0 && IsGrouping) + { + // we can't just call RemoveFromSubgroups, as the group name + // for the item may have changed during the edit. + _group.RemoveItemFromSubgroupsByExhaustiveSearch(editItem); + if (PageSize > 0) + { + _temporaryGroup.RemoveItemFromSubgroupsByExhaustiveSearch(editItem); + } + } + + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // only adjust currency and fire the event if we actually removed the item + if (removeIndex >= 0) + { + AdjustCurrencyForRemove(removeIndex); + + // raise the remove event so we can next insert it into the correct place + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + editItem, + removeIndex)); + } + + // check to see that the item will be added back in + bool passedFilter = PassesFilter(editItem); + + // if we removed all items from the current page, + // move to the previous page. we do not need to + // fire additional notifications, as moving the page will + // trigger a reset. + if (NeedToMoveToPreviousPage && !passedFilter) + { + MoveToPreviousPage(); + return; + } + + // next process adding it into the correct location + ProcessInsertToCollection(editItem, internalRemoveIndex); + + int pageStartIndex = PageIndex * PageSize; + int nextPageStartIndex = pageStartIndex + PageSize; + + if (IsGrouping) + { + int leafIndex = -1; + if (passedFilter && PageSize > 0) + { + _temporaryGroup.AddToSubgroups(editItem, false /*loading*/); + leafIndex = _temporaryGroup.LeafIndexOf(editItem); + } + + // if we are not paging, we should just be able to add the item. + // otherwise, we need to validate that it is within the current page. + if (passedFilter && (PageSize == 0 || + (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex))) + { + _group.AddToSubgroups(editItem, false /*loading*/); + int addIndex = IndexOf(editItem); + AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + editItem, + addIndex)); + } + else if (PageSize > 0) + { + int addIndex = -1; + if (passedFilter && leafIndex < pageStartIndex) + { + // if the item was added to an earlier page, then we need to bring + // in the item that would have been pushed down to this page + addIndex = pageStartIndex; + } + else if (!OnLastLocalPage && removeIndex >= 0) + { + // if the item was added to a later page, then we need to bring in the + // first item from the next page + addIndex = nextPageStartIndex - 1; + } + + object addItem = _temporaryGroup.LeafAt(addIndex); + if (addItem != null) + { + _group.AddToSubgroups(addItem, false /*loading*/); + addIndex = IndexOf(addItem); + AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + addItem, + addIndex)); + } + } + } + else + { + // if we are still within the view + int addIndex = IndexOf(editItem); + if (addIndex >= 0) + { + AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + editItem, + addIndex)); + } + else if (PageSize > 0) + { + // calculate whether the item was inserted into the previous page + bool insertedToPreviousPage = PassesFilter(editItem) && + (InternalIndexOf(editItem) < ConvertToInternalIndex(0)); + addIndex = insertedToPreviousPage ? 0 : Count - 1; + + // don't fire the event if we are on the last page + // and we don't have any items to bring in. + if (insertedToPreviousPage || (!OnLastLocalPage && removeIndex >= 0)) + { + AdjustCurrencyForEdit(restoreCurrencyTo, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + GetItemAt(addIndex), + addIndex)); + } + } + } + + // now raise currency changes at the end + RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + else if (!Contains(editItem)) + { + // if the item did not belong to the collection, add it + InternalList.Add(editItem); + } + } + + /// + /// Complete the transaction started by AddNew. We follow the WPF + /// convention in that the view's sort, filter, and paging + /// specifications (if any) are applied to the new item. + /// + //TODO Paging + public void CommitNew() + { + if (IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedText(nameof(CommitNew), nameof(EditItem))); + } + + VerifyRefreshNotDeferred(); + + if (CurrentAddItem == null) + { + return; + } + + // End the AddNew transaction + object newItem = EndAddNew(false); + + // keep track of the current item + object previousCurrentItem = CurrentItem; + + // Modify our _trackingEnumerator so that it shows that our collection is "up to date" + // and will not refresh for now. + _trackingEnumerator = _sourceCollection.GetEnumerator(); + + if (UsesLocalArray) + { + // first remove the item from the array so that we can insert into the correct position + int removeIndex = Count - 1; + int internalIndex = _internalList.IndexOf(newItem); + _internalList.Remove(newItem); + + if (IsGrouping) + { + _group.RemoveSpecialItem(_group.Items.Count - 1, newItem, false); + if (PageSize > 0) + { + _temporaryGroup.RemoveSpecialItem(_temporaryGroup.Items.Count - 1, newItem, false); + } + } + + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForRemove(removeIndex); + + // raise the remove event so we can next insert it into the correct place + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + newItem, + removeIndex)); + + // check to see that the item will be added back in + bool passedFilter = PassesFilter(newItem); + + // next process adding it into the correct location + ProcessInsertToCollection(newItem, internalIndex); + + int pageStartIndex = PageIndex * PageSize; + int nextPageStartIndex = pageStartIndex + PageSize; + + if (IsGrouping) + { + int leafIndex = -1; + if (passedFilter && PageSize > 0) + { + _temporaryGroup.AddToSubgroups(newItem, false /*loading*/); + leafIndex = _temporaryGroup.LeafIndexOf(newItem); + } + + // if we are not paging, we should just be able to add the item. + // otherwise, we need to validate that it is within the current page. + if (passedFilter && (PageSize == 0 || + (pageStartIndex <= leafIndex && nextPageStartIndex > leafIndex))) + { + _group.AddToSubgroups(newItem, false /*loading*/); + int addIndex = IndexOf(newItem); + + // adjust currency to either the previous current item if possible + // or to the item at the end of the list where the new item was. + if (previousCurrentItem != null) + { + if (Contains(previousCurrentItem)) + { + AdjustCurrencyForAdd(previousCurrentItem, addIndex); + } + else + { + AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex); + } + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + newItem, + addIndex)); + } + else + { + if (!passedFilter && (PageSize == 0 || OnLastLocalPage)) + { + AdjustCurrencyForRemove(removeIndex); + } + else if (PageSize > 0) + { + int addIndex = -1; + if (passedFilter && leafIndex < pageStartIndex) + { + // if the item was added to an earlier page, then we need to bring + // in the item that would have been pushed down to this page + addIndex = pageStartIndex; + } + else if (!OnLastLocalPage) + { + // if the item was added to a later page, then we need to bring in the + // first item from the next page + addIndex = nextPageStartIndex - 1; + } + + object addItem = _temporaryGroup.LeafAt(addIndex); + if (addItem != null) + { + _group.AddToSubgroups(addItem, false /*loading*/); + addIndex = IndexOf(addItem); + + // adjust currency to either the previous current item if possible + // or to the item at the end of the list where the new item was. + if (previousCurrentItem != null) + { + if (Contains(previousCurrentItem)) + { + AdjustCurrencyForAdd(previousCurrentItem, addIndex); + } + else + { + AdjustCurrencyForAdd(GetItemAt(Count - 1), addIndex); + } + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + addItem, + addIndex)); + } + } + } + } + else + { + // if we are still within the view + int addIndex = IndexOf(newItem); + if (addIndex >= 0) + { + AdjustCurrencyForAdd(newItem, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + newItem, + addIndex)); + } + else + { + if (!passedFilter && (PageSize == 0 || OnLastLocalPage)) + { + AdjustCurrencyForRemove(removeIndex); + } + else if (PageSize > 0) + { + bool insertedToPreviousPage = InternalIndexOf(newItem) < ConvertToInternalIndex(0); + addIndex = insertedToPreviousPage ? 0 : Count - 1; + + // don't fire the event if we are on the last page + // and we don't have any items to bring in. + if (insertedToPreviousPage || !OnLastLocalPage) + { + AdjustCurrencyForAdd(null, addIndex); + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + GetItemAt(addIndex), + addIndex)); + } + } + } + } + + // we want to fire the current changed event, even if we kept + // the same current item and position, since the item was + // removed/added back to the collection + RaiseCurrencyChanges(true, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + } + + /// + /// Return true if the item belongs to this view. No assumptions are + /// made about the item. This method will behave similarly to IList.Contains(). + /// If the caller knows that the item belongs to the + /// underlying collection, it is more efficient to call PassesFilter. + /// + /// The item we are checking to see whether it is within the collection + /// Boolean value of whether or not the collection contains the item + public bool Contains(object item) + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + return IndexOf(item) >= 0; + } + + /// + /// Enter a Defer Cycle. + /// Defer cycles are used to coalesce changes to the ICollectionView. + /// + /// IDisposable used to notify that we no longer need to defer, when we dispose + public IDisposable DeferRefresh() + { + if (IsAddingNew || IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(DeferRefresh))); + } + + ++_deferLevel; + return new DeferHelper(this); + } + + /// + /// Begins an editing transaction on the given item. The transaction is + /// completed by calling either CommitEdit or CancelEdit. Any changes made + /// to the item during the transaction are considered "pending", provided + /// that the view supports the notion of "pending changes" for the given item. + /// + /// Item we want to edit + public void EditItem(object item) + { + VerifyRefreshNotDeferred(); + + if (IsAddingNew) + { + if (Object.Equals(item, CurrentAddItem)) + { + // EditItem(newItem) is a no-op + return; + } + + // implicitly close a previous AddNew + CommitNew(); + } + + // implicitly close a previous EditItem transaction + CommitEdit(); + + CurrentEditItem = item; + + if (item is IEditableObject ieo) + { + ieo.BeginEdit(); + } + } + + /// + /// Implementation of IEnumerable.GetEnumerator(). + /// This provides a way to enumerate the members of the collection + /// without changing the currency. + /// + /// IEnumerator for the collection + //TODO Paging + public IEnumerator GetEnumerator() + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + + if (IsGrouping) + { + return RootGroup?.GetLeafEnumerator(); + } + + // if we are paging + if (PageSize > 0) + { + List list = new List(); + + // if we are in the middle of asynchronous load + if (PageIndex < 0) + { + return list.GetEnumerator(); + } + + for (int index = _pageSize * PageIndex; + index < (int)Math.Min(_pageSize * (PageIndex + 1), InternalList.Count); + index++) + { + list.Add(InternalList[index]); + } + + return new NewItemAwareEnumerator(this, list.GetEnumerator(), CurrentAddItem); + } + else + { + return new NewItemAwareEnumerator(this, InternalList.GetEnumerator(), CurrentAddItem); + } + } + + /// + /// Interface Implementation for GetEnumerator() + /// + /// IEnumerator that we get from our internal collection + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Retrieve item at the given zero-based index in this PagedCollectionView, after the source collection + /// is filtered, sorted, and paged. + /// + /// + /// Thrown if index is out of range + /// + /// Index of the item we want to retrieve + /// Item at specified index + public object GetItemAt(int index) + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + + // for indicies larger than the count + if (index >= Count || index < 0) + { + throw new ArgumentOutOfRangeException("index"); + } + + if (IsGrouping) + { + return RootGroup?.LeafAt(_isUsingTemporaryGroup ? ConvertToInternalIndex(index) : index); + } + + if (IsAddingNew && UsesLocalArray && index == Count - 1) + { + return CurrentAddItem; + } + + return InternalItemAt(ConvertToInternalIndex(index)); + } + + /// + /// Return the index where the given item appears, or -1 if doesn't appear. + /// + /// Item we are searching for + /// Index of specified item + //TODO Paging + public int IndexOf(object item) + { + EnsureCollectionInSync(); + VerifyRefreshNotDeferred(); + + if (IsGrouping) + { + return RootGroup?.LeafIndexOf(item) ?? -1; + } + if (IsAddingNew && Object.Equals(item, CurrentAddItem) && UsesLocalArray) + { + return Count - 1; + } + + int internalIndex = InternalIndexOf(item); + + if (PageSize > 0 && internalIndex != -1) + { + if ((internalIndex >= (PageIndex * _pageSize)) && + (internalIndex < ((PageIndex + 1) * _pageSize))) + { + return internalIndex - (PageIndex * _pageSize); + } + else + { + return -1; + } + } + else + { + return internalIndex; + } + } + + /// + /// Move to the given item. + /// + /// Item we want to move the currency to + /// Whether the operation was successful + public bool MoveCurrentTo(object item) + { + VerifyRefreshNotDeferred(); + + // if already on item, don't do anything + if (Object.Equals(CurrentItem, item)) + { + // also check that we're not fooled by a false null currentItem + if (item != null || IsCurrentInView) + { + return IsCurrentInView; + } + } + + // if the item is not found IndexOf() will return -1, and + // the MoveCurrentToPosition() below will move current to BeforeFirst + // The IndexOf function takes into account paging, filtering, and sorting + return MoveCurrentToPosition(IndexOf(item)); + } + + /// + /// Move to the first item. + /// + /// Whether the operation was successful + public bool MoveCurrentToFirst() + { + VerifyRefreshNotDeferred(); + + return MoveCurrentToPosition(0); + } + + /// + /// Move to the last item. + /// + /// Whether the operation was successful + public bool MoveCurrentToLast() + { + VerifyRefreshNotDeferred(); + + int index = Count - 1; + + return MoveCurrentToPosition(index); + } + + /// + /// Move to the next item. + /// + /// Whether the operation was successful + public bool MoveCurrentToNext() + { + VerifyRefreshNotDeferred(); + + int index = CurrentPosition + 1; + + if (index <= Count) + { + return MoveCurrentToPosition(index); + } + else + { + return false; + } + } + + /// + /// Move CurrentItem to this index + /// + /// Position we want to move the currency to + /// True if the resulting CurrentItem is an item within the view; otherwise False + public bool MoveCurrentToPosition(int position) + { + VerifyRefreshNotDeferred(); + + // We want to allow the user to set the currency to just + // beyond the last item. EnumerableCollectionView in WPF + // also checks (position > Count) though the ListCollectionView + // looks for (position >= Count). + if (position < -1 || position > Count) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + if ((position != CurrentPosition || !IsCurrentInSync) + && OkToChangeCurrent()) + { + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + SetCurrentToPosition(position); + OnCurrentChanged(); + + if (IsCurrentAfterLast != oldIsCurrentAfterLast) + { + OnPropertyChanged(nameof(IsCurrentAfterLast)); + } + + if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) + { + OnPropertyChanged(nameof(IsCurrentBeforeFirst)); + } + + OnPropertyChanged(nameof(CurrentPosition)); + OnPropertyChanged(nameof(CurrentItem)); + } + + return IsCurrentInView; + } + + /// + /// Move to the previous item. + /// + /// Whether the operation was successful + public bool MoveCurrentToPrevious() + { + VerifyRefreshNotDeferred(); + + int index = CurrentPosition - 1; + + if (index >= -1) + { + return MoveCurrentToPosition(index); + } + else + { + return false; + } + } + + /// + /// Moves to the first page. + /// + /// Whether or not the move was successful. + //TODO Paging + public bool MoveToFirstPage() + { + return MoveToPage(0); + } + + /// + /// Moves to the last page. + /// The move is only attempted when TotalItemCount is known. + /// + /// Whether or not the move was successful. + //TODO Paging + public bool MoveToLastPage() + { + if (TotalItemCount != -1 && PageSize > 0) + { + return MoveToPage(PageCount - 1); + } + else + { + return false; + } + } + + /// + /// Moves to the page after the current page we are on. + /// + /// Whether or not the move was successful. + //TODO Paging + public bool MoveToNextPage() + { + return MoveToPage(_pageIndex + 1); + } + + /// + /// Requests a page move to page . + /// + /// Index of the target page + /// Whether or not the move was successfully initiated. + //TODO Paging + public bool MoveToPage(int pageIndex) + { + // Boundary checks for negative pageIndex + if (pageIndex < -1) + { + return false; + } + + // if the Refresh is deferred, cache the requested PageIndex so that we + // can move to the desired page when EndDefer is called. + if (IsRefreshDeferred) + { + // set cached value and flag so that we move to the page on EndDefer + _cachedPageIndex = pageIndex; + SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); + return false; + } + + // check for invalid pageIndex + if (pageIndex == -1 && PageSize > 0) + { + return false; + } + + // Check if the target page is out of bound, or equal to the current page + if (pageIndex >= PageCount || _pageIndex == pageIndex) + { + return false; + } + + // Check with the ICollectionView.CurrentChanging listeners if it's OK to move + // on to another page + if (!OkToChangeCurrent()) + { + return false; + } + + if (RaisePageChanging(pageIndex) && pageIndex != -1) + { + // Page move was cancelled. Abort the move, but only if the target index isn't -1. + return false; + } + + // Check if there is a current edited or new item so changes can be committed first. + if (CurrentAddItem != null || CurrentEditItem != null) + { + // Remember current currency values for upcoming OnPropertyChanged notifications + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // Currently CommitNew()/CommitEdit()/CancelNew()/CancelEdit() can't handle committing or + // cancelling an item that is no longer on the current page. That's acceptable and means that + // the potential _newItem or _editItem needs to be committed before this page move. + // The reason why we temporarily reset currency here is to give a chance to the bound + // controls to commit or cancel their potential edits/addition. The DataForm calls ForceEndEdit() + // for example as a result of changing currency. + SetCurrentToPosition(-1); + RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // If the bound controls did not successfully end their potential item editing/addition, the + // page move needs to be aborted. + if (CurrentAddItem != null || CurrentEditItem != null) + { + // Since PageChanging was raised and not cancelled, a PageChanged notification needs to be raised + // even though the PageIndex actually did not change. + RaisePageChanged(); + + // Restore original currency + Debug.Assert(CurrentItem == null, "Unexpected CurrentItem != null"); + Debug.Assert(CurrentPosition == -1, "Unexpected CurrentPosition != -1"); + Debug.Assert(IsCurrentBeforeFirst, "Unexpected IsCurrentBeforeFirst == false"); + Debug.Assert(!IsCurrentAfterLast, "Unexpected IsCurrentAfterLast == true"); + + SetCurrentToPosition(oldCurrentPosition); + RaiseCurrencyChanges(false /*fireChangedEvent*/, null /*oldCurrentItem*/, -1 /*oldCurrentPosition*/, + true /*oldIsCurrentBeforeFirst*/, false /*oldIsCurrentAfterLast*/); + + return false; + } + + // Finally raise a CurrentChanging notification for the upcoming currency change + // that will occur in CompletePageMove(pageIndex). + OnCurrentChanging(); + } + + IsPageChanging = true; + CompletePageMove(pageIndex); + + return true; + } + + /// + /// Moves to the page before the current page we are on. + /// + /// Whether or not the move was successful. + //TODO Paging + public bool MoveToPreviousPage() + { + return MoveToPage(_pageIndex - 1); + } + + /// + /// Return true if the item belongs to this view. The item is assumed to belong to the + /// underlying DataCollection; this method merely takes filters into account. + /// It is commonly used during collection-changed notifications to determine if the added/removed + /// item requires processing. + /// Returns true if no filter is set on collection view. + /// + /// The item to compare against the Filter + /// Whether the item passes the filter + public bool PassesFilter(object item) + { + if (Filter != null) + { + return Filter(item); + } + + return true; + } + + /// + /// Re-create the view, using any SortDescriptions and/or Filters. + /// + public void Refresh() + { + if (this is IEditableCollectionView ecv && (ecv.IsAddingNew || ecv.IsEditingItem)) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(Refresh))); + } + + RefreshInternal(); + } + + /// + /// Remove the given item from the underlying collection. It + /// needs to be in the current filtered, sorted, and paged view + /// to call + /// + /// Item we want to remove + public void Remove(object item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + } + } + + /// + /// Remove the item at the given index from the underlying collection. + /// The index is interpreted with respect to the view (filtered, sorted, + /// and paged list). + /// + /// Index of the item we want to remove + //TODO Paging + public void RemoveAt(int index) + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the collection."); + } + + if (IsEditingItem || IsAddingNew) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText(nameof(RemoveAt))); + } + else if (!CanRemove) + { + throw new InvalidOperationException("Remove/RemoveAt is not supported."); + } + + VerifyRefreshNotDeferred(); + + // convert the index from "view-relative" to "list-relative" + object item = GetItemAt(index); + + // before we remove the item, see if we are not on the last page + // and will have to bring in a new item to replace it + bool replaceItem = PageSize > 0 && !OnLastLocalPage; + + try + { + // temporarily disable the CollectionChanged event + // handler so filtering, sorting, or grouping + // doesn't get applied yet + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, false); + + if (SourceList != null) + { + SourceList.Remove(item); + } + } + finally + { + SetFlag(CollectionViewFlags.ShouldProcessCollectionChanged, true); + } + + // Modify our _trackingEnumerator so that it shows that our collection is "up to date" + // and will not refresh for now. + _trackingEnumerator = _sourceCollection.GetEnumerator(); + + Debug.Assert(index == IndexOf(item), "IndexOf returned unexpected value"); + + // remove the item from the internal list + _internalList.Remove(item); + + if (IsGrouping) + { + if (PageSize > 0) + { + _temporaryGroup.RemoveFromSubgroups(item); + } + _group.RemoveFromSubgroups(item); + } + + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForRemove(index); + + // fire remove notification + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + item, + index)); + + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // if we removed all items from the current page, + // move to the previous page. we do not need to + // fire additional notifications, as moving the page will + // trigger a reset. + if (NeedToMoveToPreviousPage) + { + MoveToPreviousPage(); + return; + } + + // if we are paging, we may have to fire another notification for the item + // that needs to replace the one we removed on this page. + if (replaceItem) + { + // we first need to add the item into the current group + if (IsGrouping) + { + object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1); + if (newItem != null) + { + _group.AddToSubgroups(newItem, loading: false); + } + } + + // fire the add notification + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + GetItemAt(PageSize - 1), + PageSize - 1)); + } + } + + #endregion Public Methods + + ////------------------------------------------------------ + //// + //// Static Methods + //// + ////------------------------------------------------------ + + #region Static Methods + + /// + /// Helper for SortList to handle nested properties (e.g. Address.Street) + /// + /// parent object + /// property names path + /// property type that we want to check for + /// child object + private static object InvokePath(object item, string propertyPath, Type propertyType) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(item, propertyPath, propertyType, out Exception exception); + if (exception != null) + { + throw exception; + } + return propertyValue; + } + + #endregion Static Methods + + ////------------------------------------------------------ + //// + //// Private Methods + //// + ////------------------------------------------------------ + + #region Private Methods + + /// + /// Fix up CurrentPosition and CurrentItem after a collection change + /// + /// Item that we want to set currency to + /// Index of item involved in the collection change + private void AdjustCurrencyForAdd(object newCurrentItem, int index) + { + if (newCurrentItem != null) + { + int newItemIndex = IndexOf(newCurrentItem); + + // if we already have the correct currency set, we don't + // want to unnecessarily fire events + if (newItemIndex >= 0 && (newItemIndex != CurrentPosition || !IsCurrentInSync)) + { + OnCurrentChanging(); + SetCurrent(newCurrentItem, newItemIndex); + } + return; + } + + if (Count == 1) + { + if (CurrentItem != null || CurrentPosition != -1) + { + // fire current changing notification + OnCurrentChanging(); + } + + // added first item; set current at BeforeFirst + SetCurrent(null, -1); + } + else if (index <= CurrentPosition) + { + // fire current changing notification + OnCurrentChanging(); + + // adjust current index if insertion is earlier + int newPosition = CurrentPosition + 1; + if (newPosition >= Count) + { + // if currency was on last item and it got shifted up, + // keep currency on last item. + newPosition = Count - 1; + } + SetCurrent(GetItemAt(newPosition), newPosition); + } + } + + /// + /// Fix up CurrentPosition and CurrentItem after a collection change + /// + /// Item that we want to set currency to + /// Index of item involved in the collection change + private void AdjustCurrencyForEdit(object newCurrentItem, int index) + { + if (newCurrentItem != null && IndexOf(newCurrentItem) >= 0) + { + OnCurrentChanging(); + SetCurrent(newCurrentItem, IndexOf(newCurrentItem)); + return; + } + + if (index <= CurrentPosition) + { + // fire current changing notification + OnCurrentChanging(); + + // adjust current index if insertion is earlier + int newPosition = CurrentPosition + 1; + if (newPosition < Count) + { + // CurrentItem might be out of sync if underlying list is not INCC + // or if this Add is the result of a Replace (Rem + Add) + SetCurrent(GetItemAt(newPosition), newPosition); + } + else + { + SetCurrent(null, Count); + } + } + } + + /// + /// Fix up CurrentPosition and CurrentItem after a collection change + /// The index can be -1 if the item was removed from a previous page + /// + /// Index of item involved in the collection change + private void AdjustCurrencyForRemove(int index) + { + // adjust current index if deletion is earlier + if (index < CurrentPosition) + { + // fire current changing notification + OnCurrentChanging(); + + SetCurrent(CurrentItem, CurrentPosition - 1); + } + + // adjust current index if > Count + if (CurrentPosition >= Count) + { + // fire current changing notification + OnCurrentChanging(); + + SetCurrentToPosition(Count - 1); + } + + // make sure that current position and item are in sync + if (!IsCurrentInSync) + { + // fire current changing notification + OnCurrentChanging(); + + SetCurrentToPosition(CurrentPosition); + } + } + + /// + /// Returns true if specified flag in flags is set. + /// + /// Flag we are checking for + /// Whether the specified flag is set + private bool CheckFlag(CollectionViewFlags flags) + { + return (_flags & flags) != 0; + } + + /// + /// Called to complete the page move operation to set the + /// current page index. + /// + /// Final page index + //TODO Paging + private void CompletePageMove(int pageIndex) + { + Debug.Assert(_pageIndex != pageIndex, "Unexpected _pageIndex == pageIndex"); + + // to see whether or not to fire an OnPropertyChanged + int oldCount = Count; + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + _pageIndex = pageIndex; + + // update the groups + if (IsGrouping && PageSize > 0) + { + PrepareGroupsForCurrentPage(); + } + + // update currency + if (Count >= 1) + { + SetCurrent(GetItemAt(0), 0); + } + else + { + SetCurrent(null, -1); + } + + IsPageChanging = false; + OnPropertyChanged(nameof(PageIndex)); + RaisePageChanged(); + + // if the count has changed + if (Count != oldCount) + { + OnPropertyChanged(nameof(Count)); + } + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Reset)); + + // Always raise CurrentChanged since the calling method MoveToPage(pageIndex) raised CurrentChanging. + RaiseCurrencyChanges(true /*fireChangedEvent*/, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + + /// + /// Convert a value for the index passed in to the index it would be + /// relative to the InternalIndex property. + /// + /// Index to convert + /// Value for the InternalIndex + //TODO Paging + private int ConvertToInternalIndex(int index) + { + Debug.Assert(index > -1, "Unexpected index == -1"); + if (PageSize > 0) + { + return (_pageSize * PageIndex) + index; + } + else + { + return index; + } + } + + /// + /// Copy all items from the source collection to the internal list for processing. + /// + private void CopySourceToInternalList() + { + _internalList = new List(); + + IEnumerator enumerator = SourceCollection.GetEnumerator(); + + while (enumerator.MoveNext()) + { + _internalList.Add(enumerator.Current); + } + } + + /// + /// Common functionality used by CommitNew, CancelNew, and when the + /// new item is removed by Remove or Refresh. + /// + /// Whether we canceled the add + /// The new item we ended adding + private object EndAddNew(bool cancel) + { + object newItem = CurrentAddItem; + + CurrentAddItem = null; // leave "adding-new" mode + + if (newItem is IEditableObject ieo) + { + if (cancel) + { + ieo.CancelEdit(); + } + else + { + ieo.EndEdit(); + } + } + + return newItem; + } + + /// + /// Subtracts from the deferLevel counter and calls Refresh() if there are no other defers + /// + private void EndDefer() + { + --_deferLevel; + + if (_deferLevel == 0) + { + if (CheckFlag(CollectionViewFlags.IsUpdatePageSizeDeferred)) + { + SetFlag(CollectionViewFlags.IsUpdatePageSizeDeferred, false); + PageSize = _cachedPageSize; + } + + if (CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) + { + SetFlag(CollectionViewFlags.IsMoveToPageDeferred, false); + MoveToPage(_cachedPageIndex); + _cachedPageIndex = -1; + } + + if (CheckFlag(CollectionViewFlags.NeedsRefresh)) + { + Refresh(); + } + } + } + + /// + /// Makes sure that the ItemConstructor is set for the correct type + /// + private void EnsureItemConstructor() + { + if (!_itemConstructorIsValid) + { + Type itemType = ItemType; + if (itemType != null) + { + _itemConstructor = itemType.GetConstructor(Type.EmptyTypes); + _itemConstructorIsValid = true; + } + } + } + + /// + /// If the IEnumerable has changed, bring the collection up to date. + /// (This isn't necessary if the IEnumerable is also INotifyCollectionChanged + /// because we keep the collection in sync incrementally.) + /// + private void EnsureCollectionInSync() + { + // if the IEnumerable is not a INotifyCollectionChanged + if (_pollForChanges) + { + try + { + _trackingEnumerator.MoveNext(); + } + catch (InvalidOperationException) + { + // When the collection has been modified, calling MoveNext() + // on the enumerator throws an InvalidOperationException, stating + // that the collection has been modified. Therefore, we know when + // to update our internal collection. + _trackingEnumerator = SourceCollection.GetEnumerator(); + RefreshOrDefer(); + } + } + } + + /// + /// Helper function used to determine the type of an item + /// + /// Whether we should use a representative item + /// The type of the items in the collection + private Type GetItemType(bool useRepresentativeItem) + { + Type collectionType = SourceCollection.GetType(); + Type[] interfaces = collectionType.GetInterfaces(); + + // Look for IEnumerable. All generic collections should implement + // We loop through the interface list, rather than call + // GetInterface(IEnumerableT), so that we handle an ambiguous match + // (by using the first match) without an exception. + for (int i = 0; i < interfaces.Length; ++i) + { + Type interfaceType = interfaces[i]; + if (interfaceType.Name == typeof(IEnumerable<>).Name) + { + // found IEnumerable<>, extract T + Type[] typeParameters = interfaceType.GetGenericArguments(); + if (typeParameters.Length == 1) + { + return typeParameters[0]; + } + } + } + + // No generic information found. Use a representative item instead. + if (useRepresentativeItem) + { + // get type of a representative item + object item = GetRepresentativeItem(); + if (item != null) + { + return item.GetType(); + } + } + + return null; + } + + /// + /// Gets a representative item from the collection + /// + /// An item that can represent the collection + private object GetRepresentativeItem() + { + if (IsEmpty) + { + return null; + } + + IEnumerator enumerator = GetEnumerator(); + while (enumerator.MoveNext()) + { + object item = enumerator.Current; + // Since this collection view does not support a NewItemPlaceholder, + // simply return the first non-null item. + if (item != null) + { + return item; + } + } + + return null; + } + + /// + /// Return index of item in the internal list. + /// + /// The item we are checking + /// Integer value on where in the InternalList the object is located + private int InternalIndexOf(object item) + { + return InternalList.IndexOf(item); + } + + /// + /// Return item at the given index in the internal list. + /// + /// The index we are checking + /// The item at the specified index + private object InternalItemAt(int index) + { + if (index >= 0 && index < InternalList.Count) + { + return InternalList[index]; + } + else + { + return null; + } + } + + /// + /// Ask listeners (via ICollectionView.CurrentChanging event) if it's OK to change currency + /// + /// False if a listener cancels the change, True otherwise + private bool OkToChangeCurrent() + { + CurrentChangingEventArgs args = new CurrentChangingEventArgs(); + OnCurrentChanging(args); + return !args.Cancel; + } + + /// + /// Notify listeners that this View has changed + /// + /// + /// CollectionViews (and sub-classes) should take their filter/sort/grouping/paging + /// into account before calling this method to forward CollectionChanged events. + /// + /// + /// The NotifyCollectionChangedEventArgs to be passed to the EventHandler + /// + //TODO Paging + private void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + unchecked + { + // invalidate enumerators because of a change + ++_timestamp; + } + + if (CollectionChanged != null) + { + if (args.Action != NotifyCollectionChangedAction.Add || PageSize == 0 || args.NewStartingIndex < Count) + { + CollectionChanged(this, args); + } + } + + // Collection changes change the count unless an item is being + // replaced within the collection. + if (args.Action != NotifyCollectionChangedAction.Replace) + { + OnPropertyChanged(nameof(Count)); + } + + bool listIsEmpty = IsEmpty; + if (listIsEmpty != CheckFlag(CollectionViewFlags.CachedIsEmpty)) + { + SetFlag(CollectionViewFlags.CachedIsEmpty, listIsEmpty); + OnPropertyChanged(nameof(IsEmpty)); + } + } + + /// + /// Raises the CurrentChanged event + /// + private void OnCurrentChanged() + { + if (CurrentChanged != null && _currentChangedMonitor.Enter()) + { + using (_currentChangedMonitor) + { + CurrentChanged(this, EventArgs.Empty); + } + } + } + + /// + /// Raise a CurrentChanging event that is not cancelable. + /// This is called by CollectionChanges (Add, Remove, and Refresh) that + /// affect the CurrentItem. + /// + /// + /// This CurrentChanging event cannot be canceled. + /// + private void OnCurrentChanging() + { + OnCurrentChanging(uncancelableCurrentChangingEventArgs); + } + + /// + /// Raises the CurrentChanging event + /// + /// + /// CancelEventArgs used by the consumer of the event. args.Cancel will + /// be true after this call if the CurrentItem should not be changed for + /// any reason. + /// + /// + /// This CurrentChanging event cannot be canceled. + /// + private void OnCurrentChanging(CurrentChangingEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException(nameof(args)); + } + + if (_currentChangedMonitor.Busy) + { + if (args.IsCancelable) + { + args.Cancel = true; + } + + return; + } + + CurrentChanging?.Invoke(this, args); + } + + /// + /// GroupBy changed handler + /// + /// CollectionViewGroup whose GroupBy has changed + /// Arguments for the NotifyCollectionChanged event + private void OnGroupByChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (IsAddingNew || IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping")); + } + + RefreshOrDefer(); + } + + /// + /// GroupDescription changed handler + /// + /// CollectionViewGroup whose GroupDescription has changed + /// Arguments for the GroupDescriptionChanged event + //TODO Paging + private void OnGroupDescriptionChanged(object sender, EventArgs e) + { + if (IsAddingNew || IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Grouping")); + } + + // we want to make sure that the data is refreshed before we try to move to a page + // since the refresh would take care of the filtering, sorting, and grouping. + RefreshOrDefer(); + + if (PageSize > 0) + { + if (IsRefreshDeferred) + { + // set cached value and flag so that we move to first page on EndDefer + _cachedPageIndex = 0; + SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); + } + else + { + MoveToFirstPage(); + } + } + } + + /// + /// Raises a PropertyChanged event. + /// + /// PropertyChangedEventArgs for this change + private void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + /// + /// Helper to raise a PropertyChanged event. + /// + /// Property name for the property that changed + private void OnPropertyChanged(string propertyName) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Sets up the ActiveComparer for the CollectionViewGroupRoot specified + /// + /// The CollectionViewGroupRoot + private void PrepareGroupingComparer(CollectionViewGroupRoot groupRoot) + { + if (groupRoot == _temporaryGroup || PageSize == 0) + { + if (groupRoot.ActiveComparer is CollectionViewGroupInternal.ListComparer listComparer) + { + listComparer.ResetList(InternalList); + } + else + { + groupRoot.ActiveComparer = new CollectionViewGroupInternal.ListComparer(InternalList); + } + } + else if (groupRoot == _group) + { + // create the new comparer based on the current _temporaryGroup + groupRoot.ActiveComparer = new CollectionViewGroupInternal.CollectionViewGroupComparer(_temporaryGroup); + } + } + + /// + /// Use the GroupDescriptions to place items into their respective groups. + /// This assumes that there is no paging, so we just group the entire collection + /// of items that the CollectionView holds. + /// + private void PrepareGroups() + { + // we should only use this method if we aren't paging + Debug.Assert(PageSize == 0, "Unexpected PageSize != 0"); + + _group.Clear(); + _group.Initialize(); + + _group.IsDataInGroupOrder = CheckFlag(CollectionViewFlags.IsDataInGroupOrder); + + // set to false so that we access internal collection items + // instead of the group items, as they have been cleared + _isGrouping = false; + + if (_group.GroupDescriptions.Count > 0) + { + for (int num = 0, count = _internalList.Count; num < count; ++num) + { + object item = _internalList[num]; + if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) + { + _group.AddToSubgroups(item, loading: true); + } + } + if (IsAddingNew) + { + _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true); + } + } + + _isGrouping = _group.GroupBy != null; + + // now we set the value to false, so that subsequent adds will insert + // into the correct groups. + _group.IsDataInGroupOrder = false; + + // reset the grouping comparer + PrepareGroupingComparer(_group); + } + + /// + /// Use the GroupDescriptions to place items into their respective groups. + /// Because of the fact that we have paging, it is possible that we are only + /// going to need a subset of the items to be displayed. However, before we + /// actually group the entire collection, we can't display the items in the + /// correct order. We therefore want to just create a temporary group with + /// the entire collection, and then using this data we can create the group + /// that is exposed with just the items we need. + /// + private void PrepareTemporaryGroups() + { + _temporaryGroup = new CollectionViewGroupRoot(this, CheckFlag(CollectionViewFlags.IsDataInGroupOrder)); + + foreach (var gd in _group.GroupDescriptions) + { + _temporaryGroup.GroupDescriptions.Add(gd); + } + + _temporaryGroup.Initialize(); + + // set to false so that we access internal collection items + // instead of the group items, as they have been cleared + _isGrouping = false; + + if (_temporaryGroup.GroupDescriptions.Count > 0) + { + for (int num = 0, count = _internalList.Count; num < count; ++num) + { + object item = _internalList[num]; + if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) + { + _temporaryGroup.AddToSubgroups(item, loading: true); + } + } + if (IsAddingNew) + { + _temporaryGroup.InsertSpecialItem(_temporaryGroup.Items.Count, CurrentAddItem, true); + } + } + + _isGrouping = _temporaryGroup.GroupBy != null; + + // reset the grouping comparer + PrepareGroupingComparer(_temporaryGroup); + } + + /// + /// Update our Groups private accessor to point to the subset of data + /// covered by the current page, or to display the entire group if paging is not + /// being used. + /// + //TODO Paging + private void PrepareGroupsForCurrentPage() + { + _group.Clear(); + _group.Initialize(); + + // set to indicate that we will be pulling data from the temporary group data + _isUsingTemporaryGroup = true; + + // since we are getting our data from the temporary group, it should + // already be in group order + _group.IsDataInGroupOrder = true; + _group.ActiveComparer = null; + + if (GroupDescriptions.Count > 0) + { + for (int num = 0, count = Count; num < count; ++num) + { + object item = GetItemAt(num); + if (item != null && (!IsAddingNew || !object.Equals(CurrentAddItem, item))) + { + _group.AddToSubgroups(item, loading: true); + } + } + if (IsAddingNew) + { + _group.InsertSpecialItem(_group.Items.Count, CurrentAddItem, true); + } + } + + // set flag to indicate that we do not need to access the temporary data any longer + _isUsingTemporaryGroup = false; + + // now we set the value to false, so that subsequent adds will insert + // into the correct groups. + _group.IsDataInGroupOrder = false; + + // reset the grouping comparer + PrepareGroupingComparer(_group); + + _isGrouping = _group.GroupBy != null; + } + + /// + /// Create, filter and sort the local index array. + /// called from Refresh(), override in derived classes as needed. + /// + /// new IEnumerable to associate this view with + /// new local array to use for this view + private IList PrepareLocalArray(IEnumerable enumerable) + { + Debug.Assert(enumerable != null, "Input list to filter/sort should not be null"); + + // filter the collection's array into the local array + List localList = new List(); + + foreach (object item in enumerable) + { + if (Filter == null || PassesFilter(item)) + { + localList.Add(item); + } + } + + // sort the local array + if (!CheckFlag(CollectionViewFlags.IsDataSorted) && SortDescriptions.Count > 0) + { + localList = SortList(localList); + } + + return localList; + } + + /// + /// Process an Add operation from an INotifyCollectionChanged event handler. + /// + /// Item added to the source collection + /// Index item was added into + //TODO Paging + private void ProcessAddEvent(object addedItem, int addIndex) + { + // item to fire remove notification for if necessary + object removeNotificationItem = null; + if (PageSize > 0 && !IsGrouping) + { + removeNotificationItem = (Count == PageSize) ? + GetItemAt(PageSize - 1) : null; + } + + // process the add by filtering and sorting the item + ProcessInsertToCollection( + addedItem, + addIndex); + + // next check if we need to add an item into the current group + bool needsGrouping = false; + if (Count == 1 && GroupDescriptions.Count > 0) + { + // if this is the first item being added + // we want to setup the groups with the + // correct element type comparer + if (PageSize > 0) + { + PrepareGroupingComparer(_temporaryGroup); + } + PrepareGroupingComparer(_group); + } + + if (IsGrouping) + { + int leafIndex = -1; + + if (PageSize > 0) + { + _temporaryGroup.AddToSubgroups(addedItem, false /*loading*/); + leafIndex = _temporaryGroup.LeafIndexOf(addedItem); + } + + // if we are not paging, we should just be able to add the item. + // otherwise, we need to validate that it is within the current page. + if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex) + { + needsGrouping = true; + + int pageStartIndex = PageIndex * PageSize; + + // if the item was inserted on a previous page + if (pageStartIndex > leafIndex && PageSize > 0) + { + addedItem = _temporaryGroup.LeafAt(pageStartIndex); + } + + // if we're grouping and have more items than the + // PageSize will allow, remove the last item + if (PageSize > 0 && _group.ItemCount == PageSize) + { + removeNotificationItem = _group.LeafAt(PageSize - 1); + _group.RemoveFromSubgroups(removeNotificationItem); + } + } + } + + // if we are paging, we may have to fire another notification for the item + // that needs to be removed for the one we added on this page. + if (PageSize > 0 && !OnLastLocalPage && + (((IsGrouping && removeNotificationItem != null) || + (!IsGrouping && (PageIndex + 1) * PageSize > InternalIndexOf(addedItem))))) + { + if (removeNotificationItem != null && removeNotificationItem != addedItem) + { + AdjustCurrencyForRemove(PageSize - 1); + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + removeNotificationItem, + PageSize - 1)); + } + } + + // if we need to add the item into the current group + // that will be displayed + //if (needsGrouping) + //{ + // _group.AddToSubgroups(addedItem, false /*loading*/); + //} + + int addedIndex = IndexOf(addedItem); + + // if the item is within the current page + if (addedIndex >= 0) + { + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForAdd(null, addedIndex); + + // fire add notification + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + addedItem, + addedIndex)); + + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + else if (PageSize > 0) + { + // otherwise if the item was added into a previous page + int internalIndex = InternalIndexOf(addedItem); + //int internalIndex = IsGrouping ? _group.LeafIndexOf(addedItem) : InternalIndexOf(addedItem); + if (internalIndex < ConvertToInternalIndex(0)) + { + // fire add notification for item pushed in + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + GetItemAt(0), + 0)); + } + } + } + + /// + /// Process CollectionChanged event on source collection + /// that implements INotifyCollectionChanged. + /// + /// + /// The NotifyCollectionChangedEventArgs to be processed. + /// + private void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) + { + // if we do not want to handle the CollectionChanged event, return + if (!CheckFlag(CollectionViewFlags.ShouldProcessCollectionChanged)) + { + return; + } + + if (args.Action == NotifyCollectionChangedAction.Reset) + { + // if we have no items now, clear our own internal list + if (!SourceCollection.GetEnumerator().MoveNext()) + { + _internalList.Clear(); + } + + // calling Refresh, will fire the collectionchanged event + RefreshOrDefer(); + return; + } + + object addedItem = args.NewItems?[0]; + object removedItem = args.OldItems?[0]; + + // fire notifications for removes + if (args.Action == NotifyCollectionChangedAction.Remove || + args.Action == NotifyCollectionChangedAction.Replace) + { + ProcessRemoveEvent(removedItem, args.Action == NotifyCollectionChangedAction.Replace); + } + + // fire notifications for adds + if ((args.Action == NotifyCollectionChangedAction.Add || + args.Action == NotifyCollectionChangedAction.Replace) && + (Filter == null || PassesFilter(addedItem))) + { + ProcessAddEvent(addedItem, args.NewStartingIndex); + } + if (args.Action != NotifyCollectionChangedAction.Replace) + { + OnPropertyChanged(nameof(ItemCount)); + } + } + + /// + /// Process a Remove operation from an INotifyCollectionChanged event handler. + /// + /// Item removed from the source collection + /// Whether this was part of a Replace operation + //TODO Paging + private void ProcessRemoveEvent(object removedItem, bool isReplace) + { + int internalRemoveIndex = -1; + + if (IsGrouping) + { + internalRemoveIndex = PageSize > 0 ? _temporaryGroup.LeafIndexOf(removedItem) : + _group.LeafIndexOf(removedItem); + } + else + { + internalRemoveIndex = InternalIndexOf(removedItem); + } + + int removeIndex = IndexOf(removedItem); + + // remove the item from the collection + _internalList.Remove(removedItem); + + // only fire the remove if it was removed from either the current page, or a previous page + bool needToRemove = (PageSize == 0 && removeIndex >= 0) || (internalRemoveIndex < (PageIndex + 1) * PageSize); + + if (IsGrouping) + { + if (PageSize > 0) + { + _temporaryGroup.RemoveFromSubgroups(removedItem); + } + + if (needToRemove) + { + _group.RemoveFromSubgroups(removeIndex >= 0 ? removedItem : _group.LeafAt(0)); + } + } + + if (needToRemove) + { + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + AdjustCurrencyForRemove(removeIndex); + + // fire remove notification + // if we removed from current page, remove from removeIndex, + // if we removed from previous page, remove first item (index=0) + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + removedItem, + Math.Max(0, removeIndex))); + + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + // if we removed all items from the current page, + // move to the previous page. we do not need to + // fire additional notifications, as moving the page will + // trigger a reset. + if (NeedToMoveToPreviousPage && !isReplace) + { + MoveToPreviousPage(); + return; + } + + // if we are paging, we may have to fire another notification for the item + // that needs to replace the one we removed on this page. + if (PageSize > 0 && Count == PageSize) + { + // we first need to add the item into the current group + if (IsGrouping) + { + object newItem = _temporaryGroup.LeafAt((PageSize * (PageIndex + 1)) - 1); + if (newItem != null) + { + _group.AddToSubgroups(newItem, false /*loading*/); + } + } + + // fire the add notification + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + GetItemAt(PageSize - 1), + PageSize - 1)); + } + } + } + + /// + /// Handles adding an item into the collection, and applying sorting, filtering, grouping, paging. + /// + /// Item to insert in the collection + /// Index to insert item into + private void ProcessInsertToCollection(object item, int index) + { + // first check to see if it passes the filter + if (Filter == null || PassesFilter(item)) + { + if (SortDescriptions.Count > 0) + { + var itemType = ItemType; + foreach (var sort in SortDescriptions) + sort.Initialize(itemType); + + // create the SortFieldComparer to use + var sortFieldComparer = new MergedComparer(this); + + // check if the item would be in sorted order if inserted into the specified index + // otherwise, calculate the correct sorted index + if (index < 0 || /* if item was not originally part of list */ + (index > 0 && (sortFieldComparer.Compare(item, InternalItemAt(index - 1)) < 0)) || /* item has moved up in the list */ + ((index < InternalList.Count - 1) && (sortFieldComparer.Compare(item, InternalItemAt(index)) > 0))) /* item has moved down in the list */ + { + index = sortFieldComparer.FindInsertIndex(item, _internalList); + } + } + + // make sure that the specified insert index is within the valid range + // otherwise, just add it to the end. the index can be set to an invalid + // value if the item was originally not in the collection, on a different + // page, or if it had been previously filtered out. + if (index < 0 || index > _internalList.Count) + { + index = _internalList.Count; + } + + _internalList.Insert(index, item); + } + } + + /// + /// Raises Currency Change events + /// + /// Whether to fire the CurrentChanged event even if the parameters have not changed + /// CurrentItem before processing changes + /// CurrentPosition before processing changes + /// IsCurrentBeforeFirst before processing changes + /// IsCurrentAfterLast before processing changes + private void RaiseCurrencyChanges(bool fireChangedEvent, object oldCurrentItem, int oldCurrentPosition, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast) + { + // fire events for currency changes + if (fireChangedEvent || CurrentItem != oldCurrentItem || CurrentPosition != oldCurrentPosition) + { + OnCurrentChanged(); + } + if (CurrentItem != oldCurrentItem) + { + OnPropertyChanged(nameof(CurrentItem)); + } + if (CurrentPosition != oldCurrentPosition) + { + OnPropertyChanged(nameof(CurrentPosition)); + } + if (IsCurrentAfterLast != oldIsCurrentAfterLast) + { + OnPropertyChanged(nameof(IsCurrentAfterLast)); + } + if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) + { + OnPropertyChanged(nameof(IsCurrentBeforeFirst)); + } + } + + /// + /// Raises the PageChanged event + /// + private void RaisePageChanged() + { + PageChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the PageChanging event + /// + /// Index of the requested page + /// True if the event is cancelled (e.Cancel was set to True), False otherwise + private bool RaisePageChanging(int newPageIndex) + { + EventHandler handler = PageChanging; + if (handler != null) + { + PageChangingEventArgs pageChangingEventArgs = new PageChangingEventArgs(newPageIndex); + handler(this, pageChangingEventArgs); + return pageChangingEventArgs.Cancel; + } + + return false; + } + + /// + /// Will call RefreshOverride and clear the NeedsRefresh flag + /// + private void RefreshInternal() + { + RefreshOverride(); + SetFlag(CollectionViewFlags.NeedsRefresh, false); + } + + /// + /// Refresh, or mark that refresh is needed when defer cycle completes. + /// + private void RefreshOrDefer() + { + if (IsRefreshDeferred) + { + SetFlag(CollectionViewFlags.NeedsRefresh, true); + } + else + { + RefreshInternal(); + } + } + + /// + /// Re-create the view, using any SortDescriptions. + /// Also updates currency information. + /// + //TODO Paging + private void RefreshOverride() + { + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // set IsGrouping to false + _isGrouping = false; + + // force currency off the collection (gives user a chance to save dirty information) + OnCurrentChanging(); + + // if there's no sort/filter/paging/grouping, just use the collection's array + if (UsesLocalArray) + { + try + { + // apply filtering/sorting through the PrepareLocalArray method + _internalList = PrepareLocalArray(_sourceCollection); + + // apply grouping + if (PageSize == 0) + { + PrepareGroups(); + } + else + { + PrepareTemporaryGroups(); + PrepareGroupsForCurrentPage(); + } + } + catch (TargetInvocationException e) + { + // If there's an exception while invoking PrepareLocalArray, + // we want to unwrap it and throw its inner exception + if (e.InnerException != null) + { + throw e.InnerException; + } + else + { + throw; + } + } + } + else + { + CopySourceToInternalList(); + } + + // check if PageIndex is still valid after filter/sort + if (PageSize > 0 && + PageIndex > 0 && + PageIndex >= PageCount) + { + MoveToPage(PageCount - 1); + } + + // reset currency values + ResetCurrencyValues(oldCurrentItem, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + + OnCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Reset)); + + // now raise currency changes at the end + RaiseCurrencyChanges(false, oldCurrentItem, oldCurrentPosition, oldIsCurrentBeforeFirst, oldIsCurrentAfterLast); + } + + /// + /// Set currency back to the previous value it had if possible. If the item is no longer in view + /// then either use the first item in the view, or if the list is empty, use null. + /// + /// CurrentItem before processing changes + /// IsCurrentBeforeFirst before processing changes + /// IsCurrentAfterLast before processing changes + private void ResetCurrencyValues(object oldCurrentItem, bool oldIsCurrentBeforeFirst, bool oldIsCurrentAfterLast) + { + if (oldIsCurrentBeforeFirst || IsEmpty) + { + SetCurrent(null, -1); + } + else if (oldIsCurrentAfterLast) + { + SetCurrent(null, Count); + } + else + { + // try to set currency back to old current item + // if there are duplicates, use the position of the first matching item + int newPosition = IndexOf(oldCurrentItem); + + // if the old current item is no longer in view + if (newPosition < 0) + { + // if we are adding a new item, set it as the current item, otherwise, set it to null + newPosition = 0; + + if (newPosition < Count) + { + SetCurrent(GetItemAt(newPosition), newPosition); + } + else if (!IsEmpty) + { + SetCurrent(GetItemAt(0), 0); + } + else + { + SetCurrent(null, -1); + } + } + else + { + SetCurrent(oldCurrentItem, newPosition); + } + } + } + + /// + /// Set CurrentItem and CurrentPosition, no questions asked! + /// + /// + /// CollectionViews (and sub-classes) should use this method to update + /// the Current values. + /// + /// New CurrentItem + /// New CurrentPosition + private void SetCurrent(object newItem, int newPosition) + { + int count = (newItem != null) ? 0 : (IsEmpty ? 0 : Count); + SetCurrent(newItem, newPosition, count); + } + + /// + /// Set CurrentItem and CurrentPosition, no questions asked! + /// + /// + /// This method can be called from a constructor - it does not call + /// any virtuals. The 'count' parameter is substitute for the real Count, + /// used only when newItem is null. + /// In that case, this method sets IsCurrentAfterLast to true if and only + /// if newPosition >= count. This distinguishes between a null belonging + /// to the view and the dummy null when CurrentPosition is past the end. + /// + /// New CurrentItem + /// New CurrentPosition + /// Numbers of items in the collection + private void SetCurrent(object newItem, int newPosition, int count) + { + if (newItem != null) + { + // non-null item implies position is within range. + // We ignore count - it's just a placeholder + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, false); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, false); + } + else if (count == 0) + { + // empty collection - by convention both flags are true and position is -1 + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); + newPosition = -1; + } + else + { + // null item, possibly within range. + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, newPosition < 0); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, newPosition >= count); + } + + _currentItem = newItem; + _currentPosition = newPosition; + } + + /// + /// Just move it. No argument check, no events, just move current to position. + /// + /// Position to move the current item to + private void SetCurrentToPosition(int position) + { + if (position < 0) + { + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); + SetCurrent(null, -1); + } + else if (position >= Count) + { + SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); + SetCurrent(null, Count); + } + else + { + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst | CollectionViewFlags.IsCurrentAfterLast, false); + SetCurrent(GetItemAt(position), position); + } + } + + /// + /// Sets the specified Flag(s) + /// + /// Flags we want to set + /// Value we want to set these flags to + private void SetFlag(CollectionViewFlags flags, bool value) + { + if (value) + { + _flags = _flags | flags; + } + else + { + _flags = _flags & ~flags; + } + } + + /// + /// Set new SortDescription collection; re-hook collection change notification handler + /// + /// SortDescriptionCollection to set the property value to + private void SetSortDescriptions(AvaloniaSortDescriptionCollection descriptions) + { + if (_sortDescriptions != null) + { + _sortDescriptions.CollectionChanged -= SortDescriptionsChanged; + } + + _sortDescriptions = descriptions; + + if (_sortDescriptions != null) + { + Debug.Assert(_sortDescriptions.Count == 0, "must be empty SortDescription collection"); + _sortDescriptions.CollectionChanged += SortDescriptionsChanged; + } + } + + /// + /// SortDescription was added/removed, refresh PagedCollectionView + /// + /// Sender that triggered this handler + /// NotifyCollectionChangedEventArgs for this change + private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (IsAddingNew || IsEditingItem) + { + throw new InvalidOperationException(GetOperationNotAllowedDuringAddOrEditText("Sorting")); + } + + // we want to make sure that the data is refreshed before we try to move to a page + // since the refresh would take care of the filtering, sorting, and grouping. + RefreshOrDefer(); + + if (PageSize > 0) + { + if (IsRefreshDeferred) + { + // set cached value and flag so that we move to first page on EndDefer + _cachedPageIndex = 0; + SetFlag(CollectionViewFlags.IsMoveToPageDeferred, true); + } + else + { + MoveToFirstPage(); + } + } + + OnPropertyChanged("SortDescriptions"); + } + + /// + /// Sort the List based on the SortDescriptions property. + /// + /// List of objects to sort + /// The sorted list + private List SortList(List list) + { + Debug.Assert(list != null, "Input list to sort should not be null"); + + IEnumerable seq = (IEnumerable)list; + IComparer comparer = new CultureSensitiveComparer(Culture); + var itemType = ItemType; + + + foreach (AvaloniaSortDescription sort in SortDescriptions) + { + sort.Initialize(itemType); + //string propertyPath = sort.PropertyName; + //Type propertyType = null; + + // find the propertyType from the first non-null instance + // in the list + //foreach (object item in list) + //{ + // if (item != null) + // { + // propertyType = item.GetType().GetNestedPropertyType(propertyPath); + // break; + // } + //} + + //IOrderedEnumerable orderedEnum = seq as IOrderedEnumerable; + + if(seq is IOrderedEnumerable orderedEnum) + { + seq = sort.ThenBy(orderedEnum); + } + else + { + seq = sort.OrderBy(seq); + } + + //switch (sort.Direction) + //{ + // case System.ComponentModel.ListSortDirection.Ascending: + // if (orderedEnum != null) + // { + // // thenby + // seq = orderedEnum.ThenBy(item => InvokePath(item, propertyPath, propertyType), comparer); + // } + // else + // { + // // orderby + // seq = seq.OrderBy(item => InvokePath(item, propertyPath, propertyType), comparer); + // } + + // break; + // case System.ComponentModel.ListSortDirection.Descending: + // if (orderedEnum != null) + // { + // // thenby + // seq = orderedEnum.ThenByDescending(item => InvokePath(item, propertyPath, propertyType), comparer); + // } + // else + // { + // // orderby + // seq = seq.OrderByDescending(item => InvokePath(item, propertyPath, propertyType), comparer); + // } + + // break; + // default: + // break; + //} + } + + return seq.ToList(); + } + + /// + /// Helper to validate that we are not in the middle of a DeferRefresh + /// and throw if that is the case. + /// + private void VerifyRefreshNotDeferred() + { + // If the Refresh is being deferred to change filtering or sorting of the + // data by this PagedCollectionView, then PagedCollectionView will not reflect the correct + // state of the underlying data. + if (IsRefreshDeferred) + { + throw new InvalidOperationException("Cannot change or check the contents or current position of the CollectionView while Refresh is being deferred."); + } + } + + #endregion Private Methods + + ////------------------------------------------------------ + //// + //// Private Classes + //// + ////------------------------------------------------------ + + #region Private Classes + + /// + /// Creates a comparer class that takes in a CultureInfo as a parameter, + /// which it will use when comparing strings. + /// + private class CultureSensitiveComparer : IComparer + { + /// + /// Private accessor for the CultureInfo of our comparer + /// + private CultureInfo _culture; + + /// + /// Creates a comparer which will respect the CultureInfo + /// that is passed in when comparing strings. + /// + /// The CultureInfo to use in string comparisons + public CultureSensitiveComparer(CultureInfo culture) + : base() + { + _culture = culture ?? CultureInfo.InvariantCulture; + } + + #region IComparer Members + + /// + /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. + /// + /// first item to compare + /// second item to compare + /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y + /// + /// Compares the 2 items using the specified CultureInfo for string and using the default object comparer for all other objects. + /// + public int Compare(object x, object y) + { + if (x == null) + { + if (y != null) + { + return -1; + } + return 0; + } + if (y == null) + { + return 1; + } + + // at this point x and y are not null + if (x.GetType() == typeof(string) && y.GetType() == typeof(string)) + { + return _culture.CompareInfo.Compare((string)x, (string)y); + } + else + { + return Comparer.Default.Compare(x, y); + } + } + + #endregion + } + + /// + /// Used to keep track of Defer calls on the PagedCollectionView, which + /// will prevent the user from calling Refresh() on the view. In order + /// to allow refreshes again, the user will have to call IDisposable.Dispose, + /// to end the Defer operation. + /// + private class DeferHelper : IDisposable + { + /// + /// Private reference to the CollectionView that created this DeferHelper + /// + private CollectionViewBase collectionView; + + /// + /// Initializes a new instance of the DeferHelper class + /// + /// CollectionView that created this DeferHelper + public DeferHelper(CollectionViewBase collectionView) + { + this.collectionView = collectionView; + } + + /// + /// Cleanup method called when done using this class + /// + public void Dispose() + { + if (collectionView != null) + { + collectionView.EndDefer(); + collectionView = null; + } + GC.SuppressFinalize(this); + } + } + + /// + /// A simple monitor class to help prevent re-entrant calls + /// + private class SimpleMonitor : IDisposable + { + /// + /// Whether the monitor is entered + /// + private bool entered; + + /// + /// Gets a value indicating whether we have been entered or not + /// + public bool Busy + { + get { return entered; } + } + + /// + /// Sets a value indicating that we have been entered + /// + /// Boolean value indicating whether we were already entered + public bool Enter() + { + if (entered) + { + return false; + } + + entered = true; + return true; + } + + /// + /// Cleanup method called when done using this class + /// + public void Dispose() + { + entered = false; + GC.SuppressFinalize(this); + } + } + + /// + /// IEnumerator generated using the new item taken into account + /// + private class NewItemAwareEnumerator : IEnumerator + { + private enum Position + { + /// + /// Whether the position is before the new item + /// + BeforeNewItem, + + /// + /// Whether the position is on the new item that is being created + /// + OnNewItem, + + /// + /// Whether the position is after the new item + /// + AfterNewItem + } + + /// + /// Initializes a new instance of the NewItemAwareEnumerator class. + /// + /// The PagedCollectionView we are creating the enumerator for + /// The baseEnumerator that we pass in + /// The new item we are adding to the collection + public NewItemAwareEnumerator(CollectionViewBase collectionView, IEnumerator baseEnumerator, object newItem) + { + _collectionView = collectionView; + _timestamp = collectionView.Timestamp; + _baseEnumerator = baseEnumerator; + _newItem = newItem; + } + + /// + /// Implements the MoveNext function for IEnumerable + /// + /// Whether we can move to the next item + public bool MoveNext() + { + if (_timestamp != _collectionView.Timestamp) + { + throw new InvalidOperationException("Collection was modified; enumeration operation cannot execute."); + } + + switch (_position) + { + case Position.BeforeNewItem: + if (_baseEnumerator.MoveNext() && + (_newItem == null || _baseEnumerator.Current != _newItem + || _baseEnumerator.MoveNext())) + { + // advance base, skipping the new item + } + else if (_newItem != null) + { + // if base has reached the end, move to new item + _position = Position.OnNewItem; + } + else + { + return false; + } + return true; + } + + // in all other cases, simply advance base, skipping the new item + _position = Position.AfterNewItem; + return _baseEnumerator.MoveNext() && + (_newItem == null + || _baseEnumerator.Current != _newItem + || _baseEnumerator.MoveNext()); + } + + /// + /// Gets the Current value for IEnumerable + /// + public object Current + { + get + { + return (_position == Position.OnNewItem) ? _newItem : _baseEnumerator.Current; + } + } + + /// + /// Implements the Reset function for IEnumerable + /// + public void Reset() + { + _position = Position.BeforeNewItem; + _baseEnumerator.Reset(); + } + + /// + /// CollectionView that we are creating the enumerator for + /// + private CollectionViewBase _collectionView; + + /// + /// The Base Enumerator that we are passing in + /// + private IEnumerator _baseEnumerator; + + /// + /// The position we are appending items to the enumerator + /// + private Position _position; + + /// + /// Reference to any new item that we want to add to the collection + /// + private object _newItem; + + /// + /// Timestamp to let us know whether there have been updates to the collection + /// + private int _timestamp; + } + + internal class MergedComparer + { + private readonly IComparer[] _comparers; + + public MergedComparer(AvaloniaSortDescriptionCollection coll) + { + _comparers = MakeComparerArray(coll); + } + public MergedComparer(CollectionViewBase collectionView) + : this(collectionView.SortDescriptions) + { } + + private static IComparer[] MakeComparerArray(AvaloniaSortDescriptionCollection coll) + { + return + coll.Select(c => c.Comparer) + .ToArray(); + } + + /// + /// Compares two objects and returns a value indicating whether one is less than, equal to or greater than the other. + /// + /// first item to compare + /// second item to compare + /// Negative number if x is less than y, zero if equal, and a positive number if x is greater than y + /// + /// Compares the 2 items using the list of property names and directions. + /// + public int Compare(object x, object y) + { + int result = 0; + + // compare both objects by each of the properties until property values don't match + for (int k = 0; k < _comparers.Length; ++k) + { + var comparer = _comparers[k]; + result = comparer.Compare(x, y); + + if (result != 0) + { + break; + } + } + + return result; + } + + /// + /// Steps through the given list using the comparer to find where + /// to insert the specified item to maintain sorted order + /// + /// Item to insert into the list + /// List where we want to insert the item + /// Index where we should insert into + public int FindInsertIndex(object x, IList list) + { + int min = 0; + int max = list.Count - 1; + int index; + + // run a binary search to find the right index + // to insert into. + while (min <= max) + { + index = (min + max) / 2; + + int result = Compare(x, list[index]); + if (result == 0) + { + return index; + } + else if (result > 0) + { + min = index + 1; + } + else + { + max = index - 1; + } + } + + return min; + } + } + + #endregion Private Classes + } +} diff --git a/src/Avalonia.DataGrid/Collections/ICollectionView.cs b/src/Avalonia.DataGrid/Collections/ICollectionView.cs new file mode 100644 index 0000000000..c7241d7627 --- /dev/null +++ b/src/Avalonia.DataGrid/Collections/ICollectionView.cs @@ -0,0 +1,233 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Text; + +namespace Avalonia.Collections +{ + /// Provides data for the event. + public class CurrentChangingEventArgs : EventArgs + { + private bool _cancel; + private bool _isCancelable; + + /// Initializes a new instance of the class and sets the property to true. + public CurrentChangingEventArgs() + { + Initialize(true); + } + + /// Initializes a new instance of the class and sets the property to the specified value. + /// true to disable the ability to cancel a change; false to enable cancellation. + public CurrentChangingEventArgs(bool isCancelable) + { + Initialize(isCancelable); + } + + private void Initialize(bool isCancelable) + { + _isCancelable = isCancelable; + } + + /// Gets a value that indicates whether the change can be canceled. + /// true if the event can be canceled; false if the event cannot be canceled. + public bool IsCancelable + { + get + { + return _isCancelable; + } + } + + /// Gets or sets a value that indicates whether the change should be canceled. + /// true if the event should be canceled; otherwise, false. The default is false. + /// The property value is false. + public bool Cancel + { + get + { + return _cancel; + } + set + { + if (IsCancelable) + _cancel = value; + else if (value) + throw new InvalidOperationException("CurrentChanging Cannot Be Canceled"); + } + } + } + + /// Enables collections to have the functionalities of current record management, custom sorting, filtering, and grouping. + public interface ICollectionView : IEnumerable, INotifyCollectionChanged + { + /// Gets or sets the cultural information for any operations of the view that may differ by culture, such as sorting. + /// The culture information to use during culture-sensitive operations. + CultureInfo Culture { get; set; } + + /// Indicates whether the specified item belongs to this collection view. + /// true if the item belongs to this collection view; otherwise, false. + /// The object to check. + bool Contains(object item); + + /// Gets the underlying collection. + /// The underlying collection. + IEnumerable SourceCollection { get; } + + /// Gets or sets a callback that is used to determine whether an item is appropriate for inclusion in the view. + /// A method that is used to determine whether an item is appropriate for inclusion in the view. + Func Filter { get; set; } + + /// Gets a value that indicates whether this view supports filtering by way of the property. + /// true if this view supports filtering; otherwise, false. + bool CanFilter { get; } + + /// Gets a collection of instances that describe how the items in the collection are sorted in the view. + /// A collection of values that describe how the items in the collection are sorted in the view. + AvaloniaSortDescriptionCollection SortDescriptions { get; } + + /// Gets a value that indicates whether this view supports sorting by way of the property. + /// true if this view supports sorting; otherwise, false. + bool CanSort { get; } + + /// Gets a value that indicates whether this view supports grouping by way of the property. + /// true if this view supports grouping; otherwise, false. + bool CanGroup { get; } + + /// Gets a collection of objects that describe how the items in the collection are grouped in the view. + /// A collection of objects that describe how the items in the collection are grouped in the view. + //ObservableCollection GroupDescriptions { get; } + + bool IsGrouping { get; } + int GroupingDepth { get; } + string GetGroupingPropertyNameAtDepth(int level); + + /// Gets the top-level groups. + /// A read-only collection of the top-level groups or null if there are no groups. + IAvaloniaReadOnlyList Groups { get; } + + /// Gets a value that indicates whether the view is empty. + /// true if the view is empty; otherwise, false. + bool IsEmpty { get; } + + /// Recreates the view. + void Refresh(); + + /// Enters a defer cycle that you can use to merge changes to the view and delay automatic refresh. + /// The typical usage is to create a using scope with an implementation of this method and then include multiple view-changing calls within the scope. The implementation should delay automatic refresh until after the using scope exits. + IDisposable DeferRefresh(); + + /// Gets the current item in the view. + /// The current item in the view or null if there is no current item. + object CurrentItem { get; } + + /// Gets the ordinal position of the in the view. + /// The ordinal position of the in the view. + int CurrentPosition { get; } + + /// Gets a value that indicates whether the of the view is beyond the end of the collection. + /// true if the of the view is beyond the end of the collection; otherwise, false. + bool IsCurrentAfterLast { get; } + + /// Gets a value that indicates whether the of the view is beyond the start of the collection. + /// true if the of the view is beyond the start of the collection; otherwise, false. + bool IsCurrentBeforeFirst { get; } + + /// Sets the first item in the view as the . + /// true if the resulting is an item in the view; otherwise, false. + bool MoveCurrentToFirst(); + + /// Sets the last item in the view as the . + /// true if the resulting is an item in the view; otherwise, false. + bool MoveCurrentToLast(); + + /// Sets the item after the in the view as the . + /// true if the resulting is an item in the view; otherwise, false. + bool MoveCurrentToNext(); + + /// Sets the item before the in the view to the . + /// true if the resulting is an item in the view; otherwise, false. + bool MoveCurrentToPrevious(); + + /// Sets the specified item in the view as the . + /// true if the resulting is an item in the view; otherwise, false. + /// The item to set as the current item. + bool MoveCurrentTo(object item); + + /// Sets the item at the specified index to be the in the view. + /// true if the resulting is an item in the view; otherwise, false. + /// The index to set the to. + bool MoveCurrentToPosition(int position); + + /// Occurs before the current item changes. + event EventHandler CurrentChanging; + + /// Occurs after the current item has been changed. + event EventHandler CurrentChanged; + } + public interface IEditableCollectionView + { + /// Gets a value that indicates whether a new item can be added to the collection. + /// true if a new item can be added to the collection; otherwise, false. + bool CanAddNew { get; } + + /// Adds a new item to the underlying collection. + /// The new item that is added to the collection. + object AddNew(); + + /// Ends the add transaction and saves the pending new item. + void CommitNew(); + + /// Ends the add transaction and discards the pending new item. + void CancelNew(); + + /// Gets a value that indicates whether an add transaction is in progress. + /// true if an add transaction is in progress; otherwise, false. + bool IsAddingNew { get; } + + /// Gets the item that is being added during the current add transaction. + /// The item that is being added if is true; otherwise, null. + object CurrentAddItem { get; } + + /// Gets a value that indicates whether an item can be removed from the collection. + /// true if an item can be removed from the collection; otherwise, false. + bool CanRemove { get; } + + /// Removes the item at the specified position from the collection. + /// Index of item to remove. + void RemoveAt(int index); + + /// Removes the specified item from the collection. + /// The item to remove. + void Remove(object item); + + /// Begins an edit transaction on the specified item. + /// The item to edit. + void EditItem(object item); + + /// Ends the edit transaction and saves the pending changes. + void CommitEdit(); + + /// Ends the edit transaction and, if possible, restores the original value of the item. + void CancelEdit(); + + /// Gets a value that indicates whether the collection view can discard pending changes and restore the original values of an edited object. + /// true if the collection view can discard pending changes and restore the original values of an edited object; otherwise, false. + bool CanCancelEdit { get; } + + /// Gets a value that indicates whether an edit transaction is in progress. + /// true if an edit transaction is in progress; otherwise, false. + bool IsEditingItem { get; } + + /// Gets the item in the collection that is being edited. + /// The item that is being edited if is true; otherwise, null. + object CurrentEditItem { get; } + } +} diff --git a/src/Avalonia.DataGrid/DataGrid.cs b/src/Avalonia.DataGrid/DataGrid.cs new file mode 100644 index 0000000000..33b4978596 --- /dev/null +++ b/src/Avalonia.DataGrid/DataGrid.cs @@ -0,0 +1,7045 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Collections; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.VisualTree; +using Avalonia.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Linq; +using Avalonia.Input.Platform; +using System.ComponentModel.DataAnnotations; + +namespace Avalonia.Controls +{ + /// + /// Displays data in a customizable grid. + /// + public partial class DataGrid : TemplatedControl + { + private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; + private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; + private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; + private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; + private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter"; + private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; + private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; + private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary"; + private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar"; + + private const bool DATAGRID_defaultAutoGenerateColumns = true; + internal const bool DATAGRID_defaultCanUserReorderColumns = true; + internal const bool DATAGRID_defaultCanUserResizeColumns = true; + internal const bool DATAGRID_defaultCanUserSortColumns = true; + private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected; + private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended; + + /// + /// The default order to use for columns when there is no + /// value available for the property. + /// + /// + /// The value of 10,000 comes from the DataAnnotations spec, allowing + /// some properties to be ordered at the beginning and some at the end. + /// + private const int DATAGRID_defaultColumnDisplayOrder = 10000; + + private const double DATAGRID_horizontalGridLinesThickness = 1; + private const double DATAGRID_minimumRowHeaderWidth = 4; + private const double DATAGRID_minimumColumnHeaderHeight = 4; + internal const double DATAGRID_maximumStarColumnWidth = 10000; + internal const double DATAGRID_minimumStarColumnWidth = 0.001; + private const double DATAGRID_mouseWheelDelta = 48.0; + private const double DATAGRID_maxHeadersThickness = 32768; + + private const double DATAGRID_defaultRowHeight = 22; + internal const double DATAGRID_defaultRowGroupSublevelIndent = 20; + private const double DATAGRID_defaultMinColumnWidth = 20; + private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; + + /* + // DataGrid Template Parts + private ValidationSummary _validationSummary; + + private List _indeiValidationResults; + private DataGridCellCoordinates _previousAutomationFocusCoordinates; + private List _propertyValidationResults; + private ObservableCollection
+ /// Clear a sort criteria by assigning SortDescription.Empty to this property. + /// One or more sort criteria in form of + /// can be used, each specifying a property and direction to sort by. + ///