Read/edit values starting from a Telerik RadGrid - c#

I have this situation:
... a normal RadGrid with data. And, if I clic on a row, I want to obtain this:
... a list of label-textbox pair (please pay attention: these list of data are obtained from the row, but are not part of it).
With the first RadGrid it's all okay.
Therefore, I have used a simple HTML Table for the list of pair (in the second image). This list being generated code-behind, from database.
The problem is the update of the TextBoxs: if I edit these textboxes and do clic on the Update Botton, starts the myRadGrid_UpdateCommand method. But I can't find a way to manage these textboxes (they don't appear in myRadGrid.Controls or else).
So I have tried to use another RadGrid inside the first RadGrid, but with no luck... Maybe I have to use another different Telerik control?
Someone know how I can do this?
This is part of my implementation:
protected void myRadGrid_NeedDataSource(object source, GridNeedDataSourceEventArgs e)
{
this.myRadGrid.DataSource = this.dtListaDettagli;
this.dtListaDettagli.PrimaryKey = new DataColumn[] { this.dtListaDettagli.Columns["key"] };
}
protected void myRadGrid_ItemCreated(object sender, Telerik.Web.UI.GridItemEventArgs e)
{
if (e.Item is GridEditFormItem && e.Item.IsInEditMode)
{
GridEditFormItem item = (GridEditFormItem)e.Item;
UserControl userControl = (UserControl)e.Item.FindControl(GridEditFormItem.EditFormUserControlID);
var listOfDetails = this.Session["listOfDetails"];
//...
var dtoTrav = (List<Detail_Type_N>) listOfDetails;
PopolaUC(dtoTrav, userControl, e.Item.ItemIndex);
}
}
private void PopolaUC<T>(List<T> data, UserControl uc, int index) where T : FlussiBaseDto
{
// ...
RadPane radPane = uc.FindControl("RadPane1") as RadPane;
var properties = TypeDescriptor.GetProperties(typeof(Detail_Type_N));
// ...
var dettaglioSelected = (from x in data
where x.IdFlusso == idFlussoSelected && x.ProgDettaglio == progDettaglioSelected
select x).FirstOrDefault();
HtmlTable htmlTable = new HtmlTable();
htmlTable.ID = "DettaglioSinistro";
var tRow = new HtmlTableRow();
int i = 0;
foreach (PropertyDescriptor prop in properties)
{
i++;
if (i > 3) // organizza la sottotabella in 2 colonne
{
tRow = new HtmlTableRow();
i = 1;
}
// Set label:
HtmlTableCell tLabel = new HtmlTableCell();
var stringInNormalCase = Regex.Replace(prop.Name, "(\\B[A-Z])", " $1");
tLabel.InnerText = stringInNormalCase;
tRow.Cells.Add(tLabel);
// Set TextBox:
HtmlTableCell tCell = new HtmlTableCell();
// ...
TextBox box = new TextBox();
box.Text = Convert.ToString(prop.GetValue(detailSelected));
box.ID = string.Format("my_{0}", prop.Name);
tCell.Controls.Add(box);
tRow.Cells.Add(tCell);
htmlTable.Rows.Add(tRow);
}
radPane.Controls.Add(htmlTable);
}
protected void myRadGrid_UpdateCommand(object source, GridCommandEventArgs e)
{
GridEditableItem editedItem = e.Item as GridEditableItem;
UserControl userControl = (UserControl)e.Item.FindControl(GridEditFormItem.EditFormUserControlID);
//Prepare new row to add it in the DataSource
DataRow[] changedRows = this.dtListaDettagli.Select("key = " + editedItem.OwnerTableView.DataKeyValues[editedItem.ItemIndex]["key"]);
// ... and then?
catch (Exception ex)
{
changedRows[0].CancelEdit();
Label lblError = new Label();
lblError.Text = string.Format("Errore nell'aggiornamento movimento. Errore: {0} ", ex.Message);
lblError.ForeColor = System.Drawing.Color.Red;
RadGridIpa.Controls.Add(lblError);
e.Canceled = true;
}
}

You cannot generate TextBoxes and Labels dynamically.
Instead, you want to use Edit Form.
For example,
<telerik:RadGrid ID="RadGrid1" ...>
<MasterTableView>
...
<EditFormSettings>
Place those textboxes and lables here.
</EditFormSettings>
</MasterTableView>
</telerik:RadGrid>

Related

Dynamically add item to DataGridView ComboBox Column by typing in the cell

I have a DataGridView that has a ComboBox column and I must update each ComboBox's possible values when its drop down shows. I also must make the ComboBoxes capable of having custom typed values. When a new value is typed, it should be added to the list of possible values. The problem is that I get infinitely many DataError event triggers (error message boxes), I know how to handle it by just changing a field in the DataGridViewDataErrorEventArgs object, but I know it is not the correct way to handle it:
private void DataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
e.Cancel = false;
}
If I do it in the incorrect way, after selecting a value from the drop down or typing a new value, the CellValueChanged is triggered but the closed ComboBox does not display the current value but an already existing value (the first in the list).
In the following code the Form subclass is Form2, the initial values are stored in the str field and the UpdatePossibleValues method is called to update the possible values in all the ComboBoxes inside the only column in the data grid view, a DataGridViewComboBoxColumn:
public Form2()
{
InitializeComponent();
dataGridView1.EditingControlShowing += DataGridView1_EditingControlShowing;
UpdatePossibleValues();
}
internal List<string> str = new List<string>()
{
"val1",
"val2"
};
private void DataGridView1_EditingControlShowing(object sender,
DataGridViewEditingControlShowingEventArgs e)
{
if (dataGridView1.CurrentCell == null ||
dataGridView1.CurrentCell.OwningColumn == null ||
dataGridView1.CurrentCell.OwningColumn.Name != "column1")
{
return;
}
var combo = e.Control as DataGridViewComboBoxEditingControl;
if (combo == null)
{
return;
}
var cb = combo as ComboBox;
UpdatePossibleValues(cb);
cb.DropDownStyle = ComboBoxStyle.DropDown; // this makes the ComboBoxes editable
cb.Validating += Cb_Validating;
}
private void Cb_Validating(object sender, System.ComponentModel.CancelEventArgs e)
{
var cbo = sender as ComboBox;
string t = cbo.Text;
var cell = (DataGridViewComboBoxCell)dataGridView1.CurrentCell;
// add the value to the list if it is not there
if (!string.IsNullOrEmpty(t) &&
!cbo.Items.Contains(t))
{
str.Add(t);
UpdatePossibleValues(cbo);
cell.Value = t;
e.Cancel = false;
}
}
private void UpdatePossibleValues(ComboBox cb = null)
{
if (cb == null)
{
var col = dataGridView1.Columns[0] as DataGridViewComboBoxColumn;
col.Items.Clear();
foreach (string s in str)
{
col.Items.Add(s);
}
}
else
{
cb.Items.Clear();
foreach (string s in str)
{
cb.Items.Add(s);
}
}
}
Screenshots:
To dynamically add item to DataGridViewComboBoxColumn:
Hanlde EditingControlShowing and get the DataGridViewComboBoxEditingControl
Set editing control DropDownStyle to DropDown
Handle Validating event of editing control and make sure you attach the event handler just once.
Check if the Text of the editing control doesn't exists in the items:
Add it to data source of the column
Then reset data source of the column by setting it to null and assigning data source again.
Notes:
If you have multiple combo box, make sure you use different data sources for combo boxes and update corresponding data source in validating event.
If you handle the events using anonymous method, make sure you have a correct assumption about captured variables. To make it simple, you can handle the event using a normal method.
Example
The following example shows a DataGridView having two DataGridViewComboBoxColumn which for the second one, you can add new values by typing in the combo box at run-time.
To run the example, create a Form and drop a DataGridView on a new Form and just copy and paste the following code in the form:
private List<String> comboSource1;
private List<String> comboSource2;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
comboSource1 = new List<string> { "A", "B" };
comboSource2 = new List<string> { "1", "2" };
var dt = new DataTable();
dt.Columns.Add("C1");
dt.Columns.Add("C2");
dt.Rows.Add("A", "1");
dt.Rows.Add("B", "2");
var c1 = new DataGridViewComboBoxColumn();
c1.Name = "C1";
c1.DataPropertyName = "C1";
c1.DataSource = comboSource1;
var c2 = new DataGridViewComboBoxColumn();
c2.Name = "C2";
c2.DataPropertyName = "C2";
c2.DataSource = comboSource2;
dataGridView1.Columns.AddRange(c1, c2);
this.dataGridView1.DataSource = dt;
dataGridView1.EditingControlShowing += dataGridView1_EditingControlShowing;
dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
}
private void dataGridView1_EditingControlShowing(object sender,
DataGridViewEditingControlShowingEventArgs e)
{
var dataGridView = sender as DataGridView;
if (dataGridView?.CurrentCell?.ColumnIndex != 1) return;
var comboBox = e.Control as DataGridViewComboBoxEditingControl;
if (comboBox == null) return;
comboBox.DropDownStyle = ComboBoxStyle.DropDown;
if (!true.Equals(comboBox.Tag))
{
comboBox.Tag = true;
comboBox.Validating += (obj, args) =>
{
var column = (DataGridViewComboBoxColumn)dataGridView.CurrentCell.OwningColumn;
var list = comboBox.DataSource as List<string>;
if (list == null) return;
var txt = comboBox.Text;
if (!list.Contains(txt))
{
list.Add(txt);
column.DataSource = null;
column.DataSource = list;
}
dataGridView.CurrentCell.Value = txt;
dataGridView.NotifyCurrentCellDirty(true);
};
}
}

find dynamically added controls

I'm adding dropdownlists to my page depending on a amount of database entries and when I press the button I want to get the selected values in each dropdownlist.
I tried this
foreach(DropDownList a in Form.Controls.OfType<DropDownList>())
{
Response.Write(a.SelectedValue);
}
but it doesn't find any dropdownlist on the page. Below is the code I use to add the dorpdownlists.
protected void Page_Init()
{
string product = Request.QueryString["product"];
foreach (productoption r in dbcon.GetOption(product))
{
TableRow row = new TableRow();
TableCell cel1 = new TableCell();
TableCell cel2 = new TableCell();
DropDownList dropdown1 = new DropDownList();
dropdown1.CssClass = "productdropdown";
foreach (suboption f in dbcon.GetSubOption(r.ProductOptionID))
{
dropdown1.Items.Add(f.SubOptionName + " +$" +f.SubOptionPrice);
}
cel1.Text = "<b>" + r.OptionName + "</b>";
cel2.Controls.Add(dropdown1);
row.Cells.Add(cel1);
row.Cells.Add(cel2);
Table1.Rows.Add(row);
}
TableRow row2 = new TableRow();
TableCell cell3 = new TableCell();
Button cartbutton = new Button();
cartbutton.ID = product;
cartbutton.CssClass = "btn_addcart";
cartbutton.Click += cartbutton_OnClick;
cartbutton.Text = "Add to cart";
cell3.Controls.Add(cartbutton);
row2.Cells.Add(cell3);
Table1.Rows.Add(row2);
}
foreach (TabelRow row in Table1.Rows)
{
if(row.Cells.Count > 0)
{
if (row.Cells[1].Controls.Count > 0 && row.Cells[1].Controls[0].GetType() == typeof(DropDownList))
{
Response.Write(a.SelectedValue);
}
}
}
First you should make a function that looks for a control type in a ControlCollection and returns a list of found controls. Something like that:
public List<T> GetControlsOfType<T>(ControlCollection controls)
{
List<T> ret = new List<T>();
try
{
foreach (Control control in controls)
{
if (control is T)
ret.Add((T)((object)control));
else if (control.Controls.Count > 0)
ret.AddRange(GetControlsOfType<T>(control.Controls));
}
}
catch (Exception ex)
{
//Log the exception
}
return ret;
}
and then you can get all DropDownList like that:
List<DropDownList> ret = GetControlsOfType<DropDownList>(this.Page.Controls);
I hope it helped.
You should be adding controls inside another control for example a panel
*Also you dont need to define controls at page init, you can do that at page load and they will retain their value*
protected void Page_Load(object sender, EventArgs e)
{
loadControls();
}
//For Instance lets take a dropdownlist and add it to a panel named testpanel
Protected void loadControls()
{
DropdownList ddlDynamic = new DropdownList();
//give this control an id
ddlDynamic.Id = "ddlDynamic1"; // this id is very important as the control can be found with same id
//add data to dropdownlist
//adding to the panel
testpanel.Controls.Add(ddlDynamic);
}
//Now we have to find this control on post back for instance a button click
protected void btnPreviousSet_Click(object sender, EventArgs e)
{
//this will find the control here
//we will you the same id used while creating control
DropdownList ddlDynamic1 = testpanel.FindControl("ddlDynamic1") as DropdownList;
//can resume your operation here
}

Asp.net Gridview Rowdata is coming empty and Unchecked

I have a requirement where i need to add column names and rowdatabound values dynamically .I has managed to get Column names dynamically .According to my need i want values to be displayed in the form of checkboxes in the rows of the grid either in checked form or unchecked based on condition but here every thing is coming in unchecked formate ..
I am using .net membership Role table...
Here is my code for gridview Dynamic column allocation..
protected void BindGridviewData()
{
var role = from MembershipUser u in Membership.GetAllUsers()
select new
{
User = u.UserName,
Role = string.Join(",", Roles.GetRolesForUser(u.UserName))
};
DataTable dTable = new DataTable();
string[] rolesarr = Roles.GetAllRoles();
// add column for user name
dTable.Columns.Add("Username", typeof(string));
// then add all the roles as columns
Array.ForEach(rolesarr, r => dTable.Columns.Add(r));
List<string> tempfield = new List<string>();
foreach (DataColumn column in dTable.Columns)
{
string ColName = column.ColumnName;
tempfield.Add(ColName);
}
for (int i = 0; i < tempfield.Count; i++)
{
string tempfieldname = tempfield[i];
TemplateField temp = new TemplateField();
if (i == 0)
{
temp.HeaderText = tempfieldname;
tempfieldname = string.Empty;
}
else {
temp.ItemTemplate = new MyTemplate();
temp.HeaderText = tempfieldname;
tempfieldname = string.Empty;
}
GridView1.Columns.Add(temp);
}
Now Here i am checking values to be checked or uncked..
foreach (MembershipUser u in Membership.GetAllUsers())
{
DataRow dRow = dTable.NewRow();
dRow[0] = u.UserName;
string[] roles = Roles.GetRolesForUser(u.UserName);
dRow[1] = roles.Contains("Admin") ? true : false;
dRow[2] = roles.Contains("DPAO User") ? true : false;
dRow[3] = roles.Contains("GeneralUser") ? true : false;
dTable.Rows.Add(dRow);
}
GridView1.DataSource = dTable;
GridView1.DataBind();
}
Upto here values are coming fine in dTable but once flow goes to rowdatabound event
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
}
On rowdatabound event flow automatically moves to custom control class which is ...
public class MyTemplate : ITemplate
{
public void InstantiateIn(Control container)
{
CheckBox chk = new CheckBox();
chk.ID = "chk";
container.Controls.Add(chk);
}
}
And from here all checkboxes are displaying as unchecked ...
Please guys help me ..
Thanks in advance...
Do it in this way. check where the column is true false in the rowDatabound event and the add control here only.
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if(e.Row.RowType == DataControlRowType.DataRow)
{
CheckBox cb = new CheckBox();
if(e.Row.Cells["Column Name"].toString == true ){
cb.Checked = true;
}else{
cb.Checked = false;
}
e.Row.Cells["Column Name"].Controls.Add(cb);
}
}

Retrieve text from dynamically created texboxes from repeater control selections

I am trying to pass the user selections from the controls generated by a repeater (checkboxes, dropdownlist, textboxes) to a datatable and use that as a data source to a gridview for testing and eventually to a stored procedure as a table variable parameter.
When there are no selections for some checkboxes, the corresponding textboxes are not generated and the code throws an exception (check to determine if the object is empty before calling the method).
The part that seems to be causing the issue is when I pass the text from the texboxes to the datatable. When I pass the checkbox names it works fine; I am trying to overcome this by checking if the text box control is generated but it still throws the same exception.
Is there a better way to check if the dynamic textbox is generated?
protected void Button2_Click(object sender, EventArgs e)
{
DataTable Frs = new DataTable("udtMParameters");
Frs.Columns.Add("MName", typeof(string));
Frs.Columns.Add("IsNum", typeof(string));
Frs.Columns.Add("MValue1", typeof(string));
Frs.Columns.Add("MValue2", typeof(string));
try
{
foreach (RepeaterItem i in Repeater1.Items)
{
CheckBox fn = i.FindControl("chk") as CheckBox;
CheckBox isn = i.FindControl("ChkboxIsNumeric") as CheckBox;
PlaceHolder plc = i.FindControl("PlcMFilter") as PlaceHolder;
TextBox s = i.FindControl("start") as TextBox;
TextBox l = i.FindControl("end") as TextBox;
DropDownList d = i.FindControl("value") as DropDownList;
if (fn.Checked)
{
TextBox1.Text = fn.Text;
if (isn.Checked)
{
DataRow dr = Frs.NewRow();
dr["MName"] = fn.Text;
dr["IsNum"] = "Y";
if (String.IsNullOrEmpty(s.Text))
{
dr["MValue1"] = s.Text;
}
else
{
dr["MValue1"] = " ";
}
if (String.IsNullOrEmpty(s.Text))
{
dr["MValue2"] = l.Text;
}
else
{
dr["MValue2"] = " ";
}
Frs.Rows.Add(dr);
}
else
{
DataRow dr = Frs.NewRow();
dr["MName"] = fn.Text;
dr["IsNum"] = "N";
dr["MValue1"] = "MValue1";
dr["MValue2"] = "MValue2";
Frs.Rows.Add(dr);
}
}
this.GridView1.Visible = true;
GridView1.DataSource = Frs;
GridView1.DataBind();
panel2.Enabled = true;
panel2.Visible = true;
}
}
catch (Exception ex)
{
throw ex;
}
}
Replace you casting realized with as by parenthesis, in order to localize your null object
TextBox s = (TextBox)i.FindControl("start");
TextBox l = (TextBox)i.FindControl("end");
On event of a conversion failure, casting with parentheses will raises an exception, while casting with as will yields a null.

Using PostBackUrl, Getting PreviousPage.Control()

I'm using PostBackUrl to post my control from a "firstwebpage.aspx" to a "secondwebpage.aspx" so that I would be able to generate some configuration files.
I do understand that I can make use of PreviousPage.FindControl("myControlId") method in my secondwebpage.aspx to get my control from "firstwebpage.aspx"and hence grab my data and it worked.
However, it seems that this method does not work on controls which I generated programmically during runtime while populating them in a table in my firstwebpage.aspx.
I also tried using this function Response.Write("--" + Request["TextBox1"].ToString() + "--");
And although this statement do printout the text in the textfield on TextBox1, it only return me the string value of textbox1. I am unable to cast it to a textbox control in the following format too
TextBox temptextBox = (TextBox)Request["TextBox1"];
My question is, how can I actually access the control which i generated programmically in "firstwebpage.aspx" on "secondwebpage.aspx"?
Please advice!
thanks alot!
//my panel and button in aspx
<asp:Panel ID="Panel2" runat="server"></asp:Panel>
<asp:Button ID="Button1" runat="server" Text="Generate Xml" PostBackUrl="~/WebForm2.aspx" onclick="Button1_Click" />
//this is my function to insert a line into the panel
public void createfilerow(string b, string path, bool x86check, bool x86enable, bool x64check, bool x64enable)
{
Label blank4 = new Label();
blank4.ID = "blank4";
blank4.Text = "";
Panel2.Controls.Add(blank4);
CheckBox c = new CheckBox();
c.Text = b.Replace(path, "");
c.Checked = true;
c.ID = "1a";
Panel2.Controls.Add(c);
CheckBox d = new CheckBox();
d.Checked = x86check;
d.Enabled = x86enable;
d.ID = "1b";
Panel2.Controls.Add(d);
CheckBox e = new CheckBox();
e.Checked = x64check;
e.Enabled = x64enable;
e.ID = "1c";
Panel2.Controls.Add(e);
}
//my virtual path in WebForm2.aspx
<%# PreviousPageType VirtualPath="~/WebForm1.aspx" %>
//my pageload handler
protected void Page_Load(object sender, EventArgs e)
{
if (PreviousPage != null)
{
CheckBox tempCheckbox = (CheckBox)Page.PreviousPage.FindControl("1a");
Button1.Text = tempCheckbox.Text;
}
}
//handler which will populate the panel upon clicking
protected void Button7_Click(object sender, EventArgs e)
{
//get foldername
if (!Directory.Exists(#"myfilepath" + TextBox2.Text))
{
//folder does not exist
//do required actions
return;
}
string[] x86files = null;
string[] x64files = null;
string[] x86filespath = null;
string[] x64filespath = null;
ArrayList common = new ArrayList();
if (Directory.Exists(#"myfilepath" + TextBox2.Text + "\\x86"))
x86files = Directory.GetFileSystemEntries("myfilepath" + TextBox2.Text + "\\x86");
if (Directory.Exists(#"myfilepath" + TextBox2.Text + "\\x64"))
x64files = Directory.GetFileSystemEntries("myfilepath" + TextBox2.Text + "\\x64");
//some codes to convert x64files and x86files to string[]
//The header for Panel, 4 column
Label FL = new Label();
FL.ID = "flavourid";
FL.Text = "Flavour";
Panel2.Controls.Add(FL);
Label filetext = new Label();
filetext.ID = "filenamelabel";
filetext.Text = "File(s)";
Panel2.Controls.Add(filetext);
Label label86 = new Label();
label86.ID = "label86";
label86.Text = "x86";
Panel2.Controls.Add(label86);
Label label64 = new Label();
label64.ID = "label64";
label64.Text = "x64";
Panel2.Controls.Add(label64);
//a for loop determine number of times codes have to be run
for (int a = 0; a < num; a++)
{
ArrayList location = new ArrayList();
if (//this iteration had to be run)
{
string path = null;
switch (//id of this iteration)
{
case id:
path = some network address
}
//check the current version of iternation
string version = //version type;
//get the platform of the version
string platform = //platform
if (curent version = certain type)
{
//do what is required.
//build a list
}
else
{
//normal routine
//do what is required
//build a list
}
//populating the panel with data from list
createflavourheader(a);
//create dynamic checkboxes according to the list
foreach(string s in list)
//createrow parameter is by version type and platform
createfilerow(readin, path, true, true, false, false);
}
}
}
form1.Controls.Add(Panel2);
}
Sorry can't show you the full code as it is long and I believe it should be confidential even though i wrote them all
Yes you can access, Below is an example
// On Page1.aspx I have a button for postback
<asp:Button ID="btnSubmit" runat="server" Text="Submit"
PostBackUrl="~/Page2.aspx" />
// Page1.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
TextBox t = new TextBox(); // created a TextBox
t.ID = "myTextBox"; // assigned an ID
form1.Controls.Add(t); // Add to form
}
Now on the second page I will get the value of TextBox as
// Page2.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
if (PreviousPage != null)
{
TextBox t = (TextBox) PreviousPage.FindControl("myTextBox");
string mytboxvalue = t.Text;
}
// OR
string myTextBoxValue = Request.Form["myTextBox"];
}
Updated Answer:
Panel myPanel = new Panel();
myPanel.ID = "myPanel";
TextBox t = new TextBox();
t.ID = "myTextBox";
myPanel.Controls.Add(t);
TextBox t1 = new TextBox();
t1.ID = "myTextBox1";
myPanel.Controls.Add(t1);
// Add all your child controls to your panel and at the end add your panel to your form
form1.Controls.Add(myPanel);
// on the processing page you can get the values as
protected void Page_Load(object sender, EventArgs e)
{
if (PreviousPage != null)
{
TextBox t = (TextBox) PreviousPage.FindControl("myTextBox");
string mytboxvalue = t.Text;
}
string myTextBoxValue = Request.Form["myTextBox1"];
}
I also tried using this function Response.Write("--" +
Request["TextBox1"].ToString() + "--"); And although this statement do
printout the text in the textfield on TextBox1, it only return me the
string value of textbox1. I am unable to cast it to a textbox control
in the following format too
TextBox temptextBox = (TextBox)Request["TextBox1"];
Hi lw,
I think you may try passing the type of control (e.g. 'tb') together with the content and creating a new object (e.g. TextBox) and assign it to templtexBox object.
My 20 cents.
Andy

Categories

Resources