finish lecture 13, add css

main
borb 3 weeks ago
parent 327c710e51
commit cc779adf08

@ -18,6 +18,21 @@
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
gap: 1rem; gap: 1rem;
} }
.columns32 {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 1rem;
}
.columns23 {
display: grid;
grid-template-columns: 2fr 3fr;
gap: 1rem;
}
.columns111 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
.centered { .centered {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

File diff suppressed because one or more lines are too long

@ -1,528 +1,460 @@
# Generics, IEnumerable and LINQ ---
marp: true
![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_0.png) paginate: true
math: mathjax
theme: buutti
title: N. Generics, IEnumerable and LINQ
--- ---
# Generics # Generics, IEnumerable and LINQ
When using lists, you have to define the type of data that will be stored in the list, inside the angled brackets:
List<int> numberList = new List<int>(); // This list stores variables of type int
This means that list is a __generic__ class: it can contain data of any type <!-- headingDivider: 5 -->
<!-- class: invert -->
Classes, structs, interfaces and methods can also be generic ## Generics
All the variables that are defined generic inside a generic container will be assigned a type only when the containing object/method is called * When using lists, you have to define the type of data that will be stored in the list, inside the angled brackets:
```csharp
List<int> numberList = new List<int>(); // This list stores variables of type int
```
* This means that list is a *__generic__* class: it can contain data of any type
* Classes, structs, interfaces and methods can also be generic
* All the variables that are defined generic inside a generic container will be assigned a type only when the containing object/method is called
# Generics - Creating a Generic Class ### Creating a generic class
```csharp
class GenericClassExample<T> class GenericClassExample<T>
{ {
public T value; // This value will be whatever type is specified at instantiation public T value; // This value will be whatever type is specified at instantiation
public void PrintTypeAndValue() public void PrintTypeAndValue()
{ {
Console.WriteLine Console.WriteLine
($"This class contains a variable of type {value.GetType()} and of value {value}"); ($"This class contains a variable of type {value.GetType()} and of value {value}");
} }
} }
class Program class Program
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
GenericClassExample<int> example = new GenericClassExample<int>(); GenericClassExample<int> example = new GenericClassExample<int>();
example.value = 20; example.value = 20;
example.PrintTypeAndValue(); example.PrintTypeAndValue();
} }
} }
```
![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_1.png) ![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_1.png)
# Generics - Multiple Type Parameters ### Multiple type parameters
Generic classes can receive multiple types as parameters Generic classes can receive multiple types as parameters:
```csharp
class CustomContainer<T1, T2, T3> class CustomContainer<T1, T2, T3>
{ {
public T1 First { get; set; } public T1 First { get; set; }
public T2 Second { get; set; } public T2 Second { get; set; }
public T3 Third { get; set; } public T3 Third { get; set; }
} }
class Program class Program
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
CustomContainer<int, string, DateTime> container CustomContainer<int, string, DateTime> container
= new CustomContainer<int, string, DateTime>(); = new CustomContainer<int, string, DateTime>();
container.First = 10; container.First = 10;
container.Second = "Testing."; container.Second = "Testing.";
container.Third = DateTime.Now; container.Third = DateTime.Now;
} }
} }
```
# Generics - Creating a Generic Method ### Creating a generic Method
void GenericMethodExample<T>(T value) ```csharp
void GenericMethodExample<T>(T value)
{ {
Console.WriteLine Console.WriteLine
($"This method was passed a variable of type {value.GetType()} and of value {value}."); ($"This method was passed a variable of type {value.GetType()} and of value {value}.");
} }
GenericMethodExample<string>("ABC"); GenericMethodExample<string>("ABC");
```
![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_2.png) ![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_2.png)
* ***Note:*** You could name the generic type as anything, e.g. `<GenericType>`. It is named `<T>` by convention.
You could name the generic type as anything, e.g. \<GenericType>. It is named \<T> by convention.
# Exercise 1: Initializing a Populated List ## Exercise 1: Initializing a populated list
<!-- _backgroundColor: #29366f -->
Create a generic method GetPopulatedList\<T> which takes two parameters: T value and int length, and returns a new list of type T which is populated with the value variables and has a length of length. Create a generic method `GetPopulatedList<T>` which takes two parameters: `T value` and `int length`, and returns a new list of type `T` which is populated with the `value` variables and has a length of `length`.
Test your method out with a couple of different types and lengths: Test your method out with a couple of different types and lengths:
```csharp
List<string> list = GetPopulatedList<string>("Hello, there", 10); List<string> list = GetPopulatedList<string>("Hello, there", 10);
foreach(string value in list) foreach(string value in list)
{ {
Console.WriteLine(value); Console.WriteLine(value);
} }
```
# IEnumerable ## IEnumerable
Lists and arrays are both __collections __ that implement the __IEnumerable __ interface
All objects that implement the IEnumerable interface can be iterated with the foreach statement
* Lists and arrays are both *__collections__* that implement the `IEnumerable` interface
* All objects that implement the IEnumerable interface can be iterated with the foreach statement
```csharp
IEnumerable<string> names = new string[] {"Harry", "Luke", "Harley"}; IEnumerable<string> names = new string[] {"Harry", "Luke", "Harley"};
IEnumerable<string> days = new List<string> {"Sunday", "Monday", "Friday"}; IEnumerable<string> days = new List<string> {"Sunday", "Monday", "Friday"};
foreach (string name in names) foreach (string name in names)
Console.WriteLine(name); Console.WriteLine(name);
foreach (string day in days) foreach (string day in days)
Console.WriteLine(day); Console.WriteLine(day);
```
# IEnumerable (continued) ---
The IEnumerable interface itself doesn't hold much functionality
* The IEnumerable interface itself doesn't hold much functionality:
![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_3.png) ![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_3.png)
* However, the LINQ library includes all the methods you would typically need to apply to IEnumerables, such as filtering
However, the LINQ library includes all the methods you would typically need to apply to IEnumerables, such as filtering ## LINQ
# LINQ
Some common query expressions occur repeatedly in code ### The problem
E.g. trying to find an object with a certain id from an array: * Here's an example of a common *__query__* a programmer might have to do:
* Try to find an object with a certain id from an array
```csharp
IEnumerable<User> userArray = new User[2] // Initialize a new array of users IEnumerable<User> userArray = new User[2] // Initialize a new array of users
{ {
new User { Id = 0, Name = "Rene" } , new User { Id = 0, Name = "Rene" } ,
new User { Id = 1, Name = "Ville" } new User { Id = 1, Name = "Ville" }
}; };
User FindObjectWithId(int id) User FindObjectWithId(int id)
{ {
foreach (User user in userArray) foreach (User user in userArray)
if (user.Id == id) if (user.Id == id)
return user; return user;
return null; return null;
} }
Console.WriteLine(FindObjectWithId(1).Name); // Outputs "Ville" Console.WriteLine(FindObjectWithId(1).Name); // Outputs "Ville"
```
# LINQ (continued) ---
* Having to write your own method for each possible query operation (select, filter, sort…) would of course be nonsensical
* The List class includes some methods for manipulation, but...
* These only work on lists; not all IEnumerables (e.g. arrays) contain those methods
* Add to this all the different types of data (objects, SQL databases, XML, JSON…)
To introduce extensive query capabilities to all collection types, Language-Integrated Query (LINQ) was created
LINQ supports querying of objects and even XML and SQL data, directly in your code * Having to write your own method for every possible query operation (select, filter, sort…) would be nonsensical
* The `List` class includes some methods for manipulation, but...
* These only work on lists; not all `IEnumerables` (e.g. arrays) contain those methods!
* Not to mention different data types altogether (objects, SQL databases, XML, JSON…)
* What to do?
Get started by adding the namespace to your project: ### The solution
* To introduce extensive query capabilities to all collection types, Language-Integrated Query (LINQ) was created
* LINQ supports querying of objects and even XML and SQL data, directly in your code
* Get started by adding the [System.Linq](https://learn.microsoft.com/en-us/dotnet/api/system.linq?view=net-9.0) namespace to your project:
```csharp
using System.Linq; using System.Linq;
```
Here's the first example using LINQ: ---
* Here's a solution to the earlier example that uses LINQ:
```csharp
IEnumerable<User> userArray = new User[2] // Initialize a new array of users IEnumerable<User> userArray = new User[2] // Initialize a new array of users
{ {
new User { Id = 0, Name = "Rene" } , new User { Id = 0, Name = "Rene" } ,
new User { Id = 1, Name = "Ville"} new User { Id = 1, Name = "Ville"}
}; };
Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville" Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville"
```
* The [Enumerable.First](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.first?view=net-9.0) method returns the first result that satisfies the expression in the parameters
The __First __ -method returns the first result that satisfies the expression in the parameters * The arrow syntax above is called a *__lambda expression__*
The arrow syntax above is called a __lambda expression__
# Lambda Expressions ## Lambda expressions
Lambda expressions are a quick way of writing one line methods
The "First" -method of LINQ takes a __delegate __ as a parameter
The following code would do exactly the same thing as the example in the previous slide:
// Parameter references an existing method
* Lambda expressions are a quick way of writing one-line methods
* The `First` method of LINQ takes a [delegate](11.%20Delegates%20and%20Events.md#delegates) as a parameter
* We could just declare a method beforehand, and then refer to it in `First` like this:
```csharp
bool GetUserWithId(User user) bool GetUserWithId(User user)
{ {
return user.Id == 1; return user.Id == 1;
} }
Console.WriteLine(userList.First(GetUserWithId).Name); // Outputs "Ville" Console.WriteLine(userList.First(GetUserWithId).Name); // Outputs "Ville"
```
As would the following: * To shorten the expression, we could use a delegate inside the parameters to refer to an anonymous method:
```csharp
// The delegate is created inside the parameters as an anonymous method
Console.WriteLine(userList.First(delegate (User user) Console.WriteLine(userList.First(delegate (User user)
{ return user.Id == 1; })); // Outputs "Ville" { return user.Id == 1; })); // Outputs "Ville"
```
# Lambda Expressions (continued) ### Lambda expressions compared
Using LINQs "First" -query with... Using LINQs "First" -query with...
<div class='columns111' markdown='1'>
<div markdown='1'>
...a method: ...a method:
```csharp
bool GetUserWithId(User user) bool GetUserWithId(User user)
{ {
return user.Id == 1; return user.Id == 1;
} }
Console.WriteLine( Console.WriteLine(
userList.First( userList.First(
GetUserWithId).Name); GetUserWithId).Name);
// Outputs "Ville" // Outputs "Ville"
```
</div>
<div markdown='1'>
...an anonymous method: ...an anonymous method:
```csharp
Console.WriteLine(userList.First( Console.WriteLine(userList.First(
delegate (User user) delegate (User user)
{ {
return user.Id == 1; return user.Id == 1;
})); }));
// Outputs "Ville" // Outputs "Ville"
```
</div>
<div markdown='1'>
...a lambda expression: ...a lambda expression:
```csharp
Console.WriteLine(userList.First( Console.WriteLine(userList.First(
user => user =>
user.Id == 1).Name); user.Id == 1).Name);
// Outputs "Ville" // Outputs "Ville"
```
# Lambda Expressions - Example </div>
</div>
Normal methods can also be declared using the arrow function ### Lambda expressions: An example
static void Main(string[] args) Regular methods can also be declared using the arrow function
```csharp
static void Main(string[] args)
{ {
// Method body assigned with lambda expression // Method body assigned with lambda expression
string PrintCheckUpper(bool upper, string text) =>
string PrintCheckUpper(bool upper, string text) => upper ? text.ToUpper() : text; upper ? text.ToUpper() : text;
PrintCheckUpper(true, "I'm not angry!"); // Outputs I'M NOT ANGRY! PrintCheckUpper(true, "I'm not angry!"); // Outputs I'M NOT ANGRY!
PrintCheckUpper(false, "I'm not angry!"); // Outputs I'm not angry!
} }
```
# LINQ and Lambda Expressions ### LINQ and Lambda expressions
Going back to our LINQ example, it could be written without lambda expressions:
bool GetUserWithId(User user)
{
return user.Id == 1;
}
* Going back to our LINQ example...
```csharp
IEnumerable<User> userArray = new User[2] // Initialize a new array of users IEnumerable<User> userArray = new User[2] // Initialize a new array of users
{ {
new User { Id = 0, Name = "Rene" } , new User { Id = 0, Name = "Rene" } ,
new User { Id = 1, Name = "Ville"} new User { Id = 1, Name = "Ville"}
}; };
```
* Here's a solution without lambda expressions:
```csharp
bool GetUserWithId(User user)
{
return user.Id == 1;
}
Console.WriteLine(userArray.First(GetUserWithId).Name); // Outputs "Ville" Console.WriteLine(userArray.First(GetUserWithId).Name); // Outputs "Ville"
```
Here's the shorter version with lambda expression again: * And here's the shorter version with lambda expression:
```csharp
Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville" Console.WriteLine(userArray.First(user => user.Id == 1).Name); // Outputs "Ville"
```
# LINQ Methods ### LINQ methods
LINQ contains methods for filtering, ordering, grouping, joining and selecting
Suppose we have a class Person that contains a property Country
The following performs filtering to persons object, returning only the persons whose country is of value "Finland":
* LINQ contains methods for filtering, ordering, grouping, joining and selecting
* Suppose we have a class `Person` that contains a property `Country`
* The following performs filtering to the `persons` object, returning only the persons whose country is of value "Finland":
```csharp
List<Person> persons = new List<Person> {/* Insert data here */}; List<Person> persons = new List<Person> {/* Insert data here */};
var queryResult = persons var queryResult = persons
.Where(person => person.Country == "Finland"); .Where(person => person.Country == "Finland");
```
# LINQ Methods (continued) ### LINQ methods listed
| Method | Example | Description | | Method | Example | Description |
| :-: | :-: | :-: | |:--------|:---------------------------------------------|:--------------------------------------------------------------------|
| Where | persons.Where(p => p.Country == "Finland") | Filters results based on an expression | | Where | `persons.Where(p => p.Country == "Finland")` | Filters results based on an expression |
| OrderBy | persons.OrderBy(p => p.LastName) | Orders results based on one of its properties | | OrderBy | `persons.OrderBy(p => p.LastName)` | Orders results based on one of its properties |
| Select | persons.Select(p => $"Dr. {p.LastName}") | Converts the enumerable into an another type based on an expression | | Select | `persons.Select(p => $"Dr. {p.LastName}")` | Converts the enumerable into an another type based on an expression |
| Skip | persons.Skip(1) | Skips first N elements |
| Take | persons.Take(5) | Returns N elements |
| ToList | persons.ToList() | Converts IEnumerable to a list |
| ToArray | persons.ToArray() | Converts IEnumerable to an array |
--- ---
Tässä on kaikki aritmeettiset operaattorit
| Method | Example | Description | | Method | Example | Description |
| :-: | :-: | :-: | |:--------|:--------------------|:-----------------------------------|
| Any | persons.Any(p => p.Country == "Finland") | Return true if at least one element satisfies a condition | | Skip | `persons.Skip(1)` | Skips first N elements |
| All | persons.All(p => p.Age >= 18) | Return true if all elements satisfy a condition | | Take | `persons.Take(5)` | Returns N elements |
| FirstOrDefault | persons.FirstOrDefault(p => !p.Active) | Returns the first element that satisfies a condition, or null if not found | | ToList | `persons.ToList()` | Converts `IEnumerable` to a list |
| Count | persons.Count(p => p.FirstName == "Mauri") | Returns the count of elements that satisfy a condition, can be left blank to count all | | ToArray | `persons.ToArray()` | Converts `IEnumerable` to an array |
More LINQ methods can be found [here](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=net-7.0)
--- ---
Tässä on kaikki aritmeettiset operaattorit | Method | Example | Description |
|:---------------|:---------------------------------------------|:---------------------------------------------------------------------------------------|
| Any | `persons.Any(p => p.Country == "Finland")` | Return `true` if at least one element satisfies a condition |
| All | `persons.All(p => p.Age >= 18)` | Return `true` if all elements satisfy a condition |
| FirstOrDefault | `persons.FirstOrDefault(p => !p.Active)` | Returns the first element that satisfies a condition, or `null` if not found |
| Count | `persons.Count(p => p.FirstName == "Mauri")` | Returns the count of elements that satisfy a condition, can be left blank to count all |
# LINQ - Example More LINQ methods can be found [here](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=net-7.0)
Suppose we have a csv file authors.csv in our project directory ### LINQ: A chained example
* Suppose we have a csv file `authors.csv` in our project directory.
```csharp
// using System.IO; // using System.IO;
string path = @"C:\some\path\authors.csv";
string path = @"C:\\some\\path\\authors.csv";
var authors = File.ReadAllLines(path) var authors = File.ReadAllLines(path)
.Skip(1) // Skip the first line which contains the column titles .Skip(1) // Skip the first line which contains the column titles
.Select(line => // Using lambda expression, return the new Author objects .Select(line => // Using lambda expression, return the new Author objects
{ {
var columns = line.Split(','); var columns = line.Split(',');
return new Author return new Author
{ {
Id = int.Parse(columns[0]), Id = int.Parse(columns[0]),
Name = columns[1], Name = columns[1],
Description = columns[2] Description = columns[2]
}; };
}) })
.ToList(); // Turn the resulting IEnumerable into a list .ToList(); // Turn the resulting IEnumerable into a list
```
* `authors` now contains all the authors from the original csv file!
authors is now an in-memory list containing all the authors from the original csv file. Note how methods can be chained! ## Query syntax
# Query Syntax
An alternative way of using LINQ is with the __query syntax__
The following performs filtering to persons object:
* An alternative way of using LINQ is with the *__query syntax__*
* The following performs filtering to a `persons` object:
```csharp
var queryResult = from person in persons var queryResult = from person in persons
where person.country == "Finland" where person.country == "Finland"
select person; select person;
```
* The *__range variable__* `person` works like the range variable in a `foreach` loop in that it holds a copy of the current element in the `persons` variable
* The `where` keyword specifies the condition for the filter
The __range variable __ person works like the range variable in a foreach loop in that it holds a copy of the current element in the persons variable ### LINQ queries: An example
The where keyword specifies the condition for the filter
# LINQ Queries - Example
Suppose we have a class City that contains a property Description
The following would print the descriptions of all cities that have descriptions less than 100 characters long
* Suppose we have a class City that contains a property Description
* The following would print the descriptions of all cities that have descriptions less than 100 characters long
```csharp
IEnumerable Cities = new List<City> {/* Insert data here */}; IEnumerable Cities = new List<City> {/* Insert data here */};
var filteredResult = from city in Cities var filteredResult = from city in Cities
where city.Description.Length < 100 where city.Description.Length < 100
select city; select city;
foreach (City result in filteredResult) foreach (City result in filteredResult)
Console.WriteLine(result.Description); Console.WriteLine(result.Description);
```
# Extension Methods vs Query Syntax ## Extension methods vs. query syntax
* In the previous examples, we used the query syntax of LINQ * In the previous examples, we used the query syntax of LINQ
* Both the methods and queries do pretty much the same thing * Both the methods and queries do pretty much the same thing
* It is up to you which syntax you want to use * It is up to you which syntax you want to use
* The method syntax works like any normal C# methods * The method syntax works like any normal C## methods
* The query syntax might be more approachable to those who are familiar with SQL * The query syntax might be more approachable to those who are familiar with SQL
<div class='columns' markdown='1'>
<div markdown='1'>
```csharp
var methodResult = persons var methodResult = persons
.Where(person =>
person.Country == "Finland");
```
.Where(person => person.Country == "Finland"); </div>
<div markdown='1'>
```csharp
var queryResult = from person in persons var queryResult = from person in persons
where person.Country == "Finland" where person.Country == "Finland"
select person; select person;
```
All queries listed here: [https://www.tutorialsteacher.com/linq/linq-standard-query-operators](https://www.tutorialsteacher.com/linq/linq-standard-query-operators) </div>
</div>
# ToArray() and ToList() Methods * All queries listed here: [https://www.tutorialsteacher.com/linq/linq-standard-query-operators](https://www.tutorialsteacher.com/linq/linq-standard-query-operators)
Notice that the LINQ queries return an IEnumerable ## `ToArray()` and `ToList()` methods
If you need to use arrays or lists, you need to call the ToArray() or ToList() methods: * Notice that the LINQ queries return an `IEnumerable`
* If you need to use arrays or lists, you need to call the `ToArray()` or `ToList()` methods
string[] strings = new string[] { "Timo", "Pekka", "Taina", "Kalle" }; <div class='columns32' markdown='1'>
<div markdown='1'>
```csharp
string[] strings = new string[]
{ "Timo", "Pekka", "Taina", "Kalle" };
string[] queryResult = strings string[] queryResult = strings
.Where(s => s.StartsWith('T')); .Where(s => s.StartsWith('T'));
```
![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_4.png) ![](imgs/13%20Generics%2C%20IEnumerable%20and%20LINQ_4.png)
string[] strings = new string[] { "Timo", "Pekka", "Taina", "Kalle" }; </div>
<div markdown='1'>
```csharp
string[] strings = new string[]
{ "Timo", "Pekka", "Taina", "Kalle" };
string[] queryResult = strings string[] queryResult = strings
.Where(s => s.StartsWith('T')) .Where(s => s.StartsWith('T'))
.ToArray();
```
.ToArray(); // This works No error!
# Exercise 2: Filtering Names
Download this file of names and add it to your project folder: [https://raw.githubusercontent.com/dominictarr/random-name/master/names.txt](https://raw.githubusercontent.com/dominictarr/random-name/master/names.txt)
Read all the contents into a string array with File.ReadAllLines()
Create a main loop where the user is asked for a string. Print the total number of names which contain that string.
If there are less than 10 resulting names, print the names as well
# Exercise 3: Queries on Object Lists
Expand on the exercise 2.
Create a new class User with two properties, int Id and string Name </div>
</div>
If the number of filtered names is less than 10, create a list of Users with those names and a running Id
Sort the list of users by the length of the Name property
Print the names and id:s of the users in the sorted list
# Going Further: Extension Methods ## Exercise 2: Filtering Names
<!-- _backgroundColor: #29366f -->
Recap: IEnumerable itself only contains one method * Download this file of names and add it to your project folder: [https://raw.githubusercontent.com/dominictarr/random-name/master/names.txt](https://raw.githubusercontent.com/dominictarr/random-name/master/names.txt)
* Read all the contents into a string array with `File.ReadAllLines()`
* Create a main loop where the user is asked for a string. Print the total number of names which contain that string.
* If there are less than 10 resulting names, print the names as well!
How does the LINQ library suddenly add all these methods to our Enumerables? ## Exercise 3: Queries on Object Lists
<!-- _backgroundColor: #29366f -->
This is possible with extension methods: * Expand on the exercise 2.
* Create a new class User with two properties, int Id and string Name
* If the number of filtered names is less than 10, create a list of Users with those names and a running Id
* Sort the list of users by the length of the Name property
* Print the names and id:s of the users in the sorted list
[https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) ## Going Further: Extension Methods
* Recap: `IEnumerable` itself only contains one method
* How does the LINQ library suddenly add all these methods to our Enumerables?
* This is possible with [extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods)!
Loading…
Cancel
Save