Save button does not work, when Update Panel is used - c#

I have created a bulk editable gridview web user control (BulkEditGridView.ascx) from the below link by just copy pasting, I did not include any external code:
http://blogs.msdn.com/b/mattdotson/archive/2005/11/09/real-world-gridview-bulk-editing.aspx
I added BulkEditGridView control into another web user control OrderDetailGridView.ascx and add datasource to BulkEditGridView
What I am tring to do is to have an interdependent dropdownlist and 2 textboxes inside my BulkEditGridView. (see OrderDetailGridView.ascx source code) At the moment when dropdownlist changes, textboxes remains unchanged, after save button is clicked, the corresponding values are updated and visible in textboxes.
However what I want to do is when dropdownlist changes, before clicking save button, I want to see corresponding values inside the textboxes, and then be able to save with save button.
I tried to set autpostback=true and ddl_MaterialCode_SelectedIndexChanged event to change the textboxes, but then save button does not work,
I tried to put an updatepanel, again the same, save button did not work.
Below are the source codes of my code in which save button works where textboxes updated after save is clicked.
BulkEditGridView.ascx source code (I did not add anything myself, just copy paste):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Exportal.Controls
{
public partial class BulkEditGridView : System.Web.UI.WebControls.GridView
{
private List<int> dirtyRows = new List<int>();
[IDReferenceProperty(typeof(Control))]
public string SaveButtonID
{
get
{
string val = (string)this.ViewState["SaveButtonID"];
if (val == null)
{
return string.Empty;
}
return val;
}
set
{
this.ViewState["SaveButtonID"] = value;
}
}
protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState)
{
return base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState | DataControlRowState.Edit);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
//Attach an event handler to the save button.
if (false == string.IsNullOrEmpty(this.SaveButtonID))
{
Control btn = RecursiveFindControl(this.NamingContainer, this.SaveButtonID);
if (null != btn)
{
if (btn is Button)
{
((Button)btn).Click += new EventHandler(SaveClicked);
}
}
}
}
private void SaveClicked(object sender, EventArgs e)
{
this.Save();
this.DataBind();
}
protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
{
base.InitializeRow(row, fields);
foreach (DataControlFieldCell cell in row.Cells)
{
if (cell.Controls.Count > 0)
{
AddChangedHandlers(cell.Controls);
}
}
}
private void AddChangedHandlers(ControlCollection controls)
{
foreach (Control ctrl in controls)
{
if (ctrl is TextBox)
{
((TextBox)ctrl).TextChanged += new EventHandler(this.HandleRowChanged);
}
else if (ctrl is CheckBox)
{
((CheckBox)ctrl).CheckedChanged += new EventHandler(this.HandleRowChanged);
}
else if (ctrl is DropDownList)
{
((DropDownList)ctrl).SelectedIndexChanged += new EventHandler(this.HandleRowChanged);
}
}
}
void HandleRowChanged(object sender, EventArgs args)
{
GridViewRow row = ((Control)sender).NamingContainer as GridViewRow;
if (null != row && !dirtyRows.Contains(row.RowIndex))
{
dirtyRows.Add(row.RowIndex);
}
}
public void Save()
{
foreach (int row in dirtyRows)
{
this.UpdateRow(row, false);
}
dirtyRows.Clear();
}
private Control RecursiveFindControl(Control namingcontainer, string controlName)
{
Control c = namingcontainer.FindControl(controlName);
if (c != null)
return c;
if (namingcontainer.NamingContainer != null)
return RecursiveFindControl(namingcontainer.NamingContainer, controlName);
return null;
}
}
}
OrderDetailGridView.ascx source code:
<cc1:BulkEditGridView ID="BulkEditGridView1" runat="server" AutoGenerateColumns="False"
DataKeyNames="RowNo" DataSourceID="SqlDataSource1" SaveButtonID="btn_Kaydet">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
<asp:BoundField DataField="PONumber" HeaderText="PONumber" SortExpression="PONumber" />
<asp:TemplateField HeaderText="MaterialCode" SortExpression="MaterialCode">
<EditItemTemplate>
<asp:DropDownList ID="ddl_MaterialCode" runat="server" DataSourceID="SqlDataSource2"
DataTextField="MaterialCode" DataValueField="MaterialCode" SelectedValue='<%# Bind("MaterialCode") %>'
OnSelectedIndexChanged="ddl_MaterialCode_SelectedIndexChanged" >
</asp:DropDownList>
</EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="MaterialDescription" SortExpression="MaterialDescription">
<EditItemTemplate>
<asp:TextBox ID="txt_MaterialDescription" runat="server" Text='<%# Bind("MaterialDescription") %>'
Enabled="false"></asp:TextBox>
</EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="MaterialCategory" SortExpression="MaterialCategory">
<EditItemTemplate>
<asp:TextBox ID="txt_MaterialCategory" runat="server" Text='<%# Bind("MaterialCategory") %>'
Enabled="false"></asp:TextBox>
</EditItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="UnitOfMeasure" HeaderText="UnitOfMeasure" SortExpression="UnitOfMeasure" />
<asp:BoundField DataField="Quantity" HeaderText="Quantity" SortExpression="Quantity" />
<asp:BoundField DataField="ContainerType" HeaderText="ContainerType" SortExpression="ContainerType" />
<asp:BoundField DataField="LoadingDate" HeaderText="LoadingDate" SortExpression="LoadingDate" />
</Columns>
</cc1:BulkEditGridView>
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:MyDbConn %>"
DeleteCommand="DELETE FROM [OrderDetail] WHERE [RowNo] = #RowNo" InsertCommand="INSERT INTO [OrderDetail] ([FileNo], [PONumber], [MaterialCode], [MaterialDescription], [MaterialCategory], [UnitOfMeasure], [Quantity], [ContainerType], [LoadingDate]) VALUES (#FileNo, #PONumber, #MaterialCode, #MaterialDescription, #MaterialCategory, #UnitOfMeasure, #Quantity, #ContainerType, #LoadingDate)"
SelectCommand="SELECT * FROM [OrderDetail]" UpdateCommand="UPDATE [OrderDetail] SET [FileNo] = #FileNo, [PONumber] = #PONumber, [MaterialCode] = #MaterialCode, [MaterialDescription] = #MaterialDescription, [MaterialCategory] = #MaterialCategory, [UnitOfMeasure] = #UnitOfMeasure, [Quantity] = #Quantity, [ContainerType] = #ContainerType, [LoadingDate] = #LoadingDate WHERE [RowNo] = #RowNo">
<DeleteParameters>
<asp:Parameter Name="RowNo" Type="Int32" />
</DeleteParameters>
<InsertParameters>
<asp:Parameter Name="FileNo" Type="Int32" />
<asp:Parameter Name="PONumber" Type="String" />
<asp:Parameter Name="MaterialCode" Type="String" />
<asp:Parameter Name="MaterialDescription" Type="String" />
<asp:Parameter Name="MaterialCategory" Type="String" />
<asp:Parameter Name="UnitOfMeasure" Type="String" />
<asp:Parameter Name="Quantity" Type="Int32" />
<asp:Parameter Name="ContainerType" Type="String" />
<asp:Parameter Name="LoadingDate" Type="String" />
</InsertParameters>
<UpdateParameters>
<asp:Parameter Name="FileNo" Type="Int32" />
<asp:Parameter Name="PONumber" Type="String" />
<asp:Parameter Name="MaterialCode" Type="String" />
<asp:Parameter Name="MaterialDescription" Type="String" />
<asp:Parameter Name="MaterialCategory" Type="String" />
<asp:Parameter Name="UnitOfMeasure" Type="String" />
<asp:Parameter Name="Quantity" Type="Int32" />
<asp:Parameter Name="ContainerType" Type="String" />
<asp:Parameter Name="LoadingDate" Type="String" />
<asp:Parameter Name="RowNo" Type="Int32" />
</UpdateParameters>
</asp:SqlDataSource>
<asp:SqlDataSource ID="SqlDataSource2" runat="server" ConnectionString="<%$ ConnectionStrings:MyDbConn %>"
SelectCommand="SELECT [MaterialCode] FROM [Materials]"></asp:SqlDataSource>
<asp:Button ID="btn_Kaydet" runat="server" Text="Save" />
OrderDetailGridView.ascx.cs source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using SOrderDetailData;
using System.Data;
namespace Exportal.Controls
{
public partial class OrderDetailGridView : System.Web.UI.UserControl
{
protected void ddl_MaterialCode_SelectedIndexChanged(object sender, EventArgs e)
{
DataTable dt = new DataTable();
dt = OrderDetailData.GetMaterials();
DropDownList ddl_MaterialCode = (DropDownList)sender;
GridViewRow r = (GridViewRow)ddl_MaterialCode.Parent.Parent;
TextBox txt_MaterialDescription = (TextBox)r.FindControl("txt_MaterialDescription");
TextBox txt_MaterialCategory = (TextBox)r.FindControl("txt_MaterialCategory");
txt_MaterialDescription.Text = dt.Rows[ddl_MaterialCode.SelectedIndex]["MaterialDescription"].ToString();
txt_MaterialCategory.Text = dt.Rows[ddl_MaterialCode.SelectedIndex]["MaterialCategory"].ToString();
}
}
}

You will probably need to persist List<int> dirtyRows so that it can survive the post back. I would recommend either the Cache or Session, however check out this MSDN article Nine Options for Managing Persistent User State in Your ASP.NET Application for all of the options available.

Two things you should try:
Put a break-point on the code below and debug. Check that the app is reaching this btn...
if (null != btn) {
if (btn is Button) {
((Button)btn).Click += new EventHandler(SaveClicked);
}
}
After verifying that, you should add this button to the script manager
if (null != btn) {
if (btn is Button) {
((Button)btn).Click += new EventHandler(SaveClicked);
ScriptManager.GetCurrent(Page).RegisterAsyncPostBackControl(btn);
}
}
Does this control is inside an updatepanel somehow (maybe your mastepage has an updatepanel for all the content)?

If all you want to do is populate the textboxes with the selected values from the dropdowns, I would suggest jquery...
$('.dropdownclass').change(function() {
$(this).closest('tr').find('.textboxclass').val($(this).val());
});
Then you don't have to do additonal postbacks and hopefully your button click will work.

If all you want to do is populate the textboxes with the selected values from the dropdowns, I would also suggest jquery like someone wrote before...
But if you need to do something more when the dropdown changes... You need to use the RowCommand event of your grid.
This might help RowCommand
Short story:
add OnRowCommand="myGrid_RowCommand" to the properties of the grid
add CommandName="ddlChanged" to the properties of the dropdown in the grid
if you have more than 1 command in every row of the grid the myGrid_RowCommand function should look like this
void myGrid_RowCommand(Object sender, GridViewCommandEventArgs e) {
if(e.CommandName=="ddlChanged")
{// Do something
}
if(e.CommandName == "ddl2Changed")
{// Do something else
}
...
}

Related

Bizarre issue with customized gridview control

I have what may be a rather complicated issue. I have an extended gridview control that I swear used to work all the time, but I went away for a while, came back, and it doesn't work anymore (I'm the sole programmer).
The extended gridview is designed so that it always shows a footer row (for inserting new rows). It loads and displays existing data correctly. If there are no rows, then adding the data works fine. But if I'm adding a new row to a gridview that already has existing rows, I get an issue where gvPhones.FooterRow is null, so it can't find the control I'm referencing.
Here's the extended gridview class (gotten from a stackoverflow page):
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
//https://stackoverflow.com/questions/994895/always-show-footertemplate-even-no-data/10891744#10891744
namespace WebForms.LocalCodeLibrary.Controls
{
//modified from https://stackoverflow.com/questions/3437581/show-gridview-footer-on-empty-grid
public class GridViewExtended : GridView
{
private GridViewRow _footerRow;
[DefaultValue(false), Category("Appearance"), Description("Include the footer when the table is empty")]
public bool ShowFooterWhenEmpty { get; set; }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
public override GridViewRow FooterRow
{
get
{
if ((this._footerRow == null))
{
this.EnsureChildControls();
}
return this._footerRow;
}
}
protected override int CreateChildControls(System.Collections.IEnumerable dataSource, bool dataBinding)
{
//creates all the rows that would normally be created when instantiating the grid
int returnVal = base.CreateChildControls(dataSource, dataBinding);
//if no rows were created (i.e. returnVal == 0), and we need to show the footer row, then we need to create and bind the footer row.
if (returnVal == 0 && this.ShowFooterWhenEmpty)
{
Table table = this.Controls.OfType<Table>().First<Table>();
DataControlField[] dcf = new DataControlField[this.Columns.Count];
this.Columns.CopyTo(dcf, 0);
//creates the footer row
this._footerRow = this.CreateRow(-1, -1, DataControlRowType.Footer, DataControlRowState.Normal, dataBinding, null, dcf, table.Rows, null);
if (!this.ShowFooter)
{
_footerRow.Visible = false;
}
}
return returnVal;
}
private GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState, bool dataBind, object dataItem, DataControlField[] fields, TableRowCollection rows, PagedDataSource pagedDataSource)
{
GridViewRow row = this.CreateRow(rowIndex, dataSourceIndex, rowType, rowState);
GridViewRowEventArgs e = new GridViewRowEventArgs(row);
if ((rowType != DataControlRowType.Pager))
{
this.InitializeRow(row, fields);
}
else
{
this.InitializePager(row, fields.Length, pagedDataSource);
}
//if the row has data, sets the data item
if (dataBind)
{
row.DataItem = dataItem;
}
//Raises the RowCreated event
this.OnRowCreated(e);
//adds the row to the gridview's row collection
rows.Add(row);
//explicitly binds the data item to the row, including the footer row and raises the RowDataBound event.
if (dataBind)
{
row.DataBind();
this.OnRowDataBound(e);
row.DataItem = null;
}
return row;
}
}
}
Here's the relevant stuff in the ASPX page:
<%# Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="ContactEdit.aspx.cs" Inherits="WebForms.Directory.ContactEdit" %>
<%# Register TagPrefix="gcctl" Namespace="WebForms.LocalCodeLibrary.Controls" Assembly="WebForms" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Head" runat="server">
<style>
</style>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div id="bodycontent" class="body-content">
<h2><asp:Literal ID="formheader" runat="server" /></h2>
<!-- Start: Main Customer section -->
<asp:Panel ID="mainformcontent" runat="server" CssClass="formsection">
<section id="mainform" class="simplelayoutform">
<asp:TextBox id="customerid" type="hidden" runat="server" />
<asp:TextBox id="contactid" type="hidden" runat="server" />
</section>
</asp:Panel>
<!-- End: Main Customer section -->
<!-- Start: Phones section -->
<asp:SqlDataSource ID="gvPhonesDataSource" runat="server" OnInserted="gvPhonesDataSource_Inserted"
ConnectionString="<%$ ConnectionStrings:ConnString %>"
SelectCommand="SELECT p.[CustomerPhoneID]
,p.[CustomerID]
,LTRIM(COALESCE(cc.FirstName,'') + ' ' + COALESCE(cc.LastName,'')) AS ContactFullName
,p.CustomerContactID
,p.PhoneTypeID
,lp.PhoneType
,p.[PhoneNumber]
,p.[Extension]
,p.[FormattedPhone]
,p.[IsActive]
,CASE WHEN p.LocationID IS NULL THEN CASE WHEN p.CustomerContactID IS NULL THEN 0 ELSE 1 END ELSE 2 END AS SortOrder
FROM [dbo].[Phones] p
LEFT JOIN dbo.Contacts cc ON p.CustomerContactID = cc.CustomerContactID
LEFT JOIN list.PhoneTypes lp ON p.PhoneTypeID = lp.PhoneTypeID
WHERE p.CustomerContactID = #CustomerContactID"
DeleteCommand="DELETE FROM [dbo].[Phones] WHERE [CustomerPhoneID] = #CustomerPhoneID"
InsertCommand="INSERT INTO [dbo].[Phones] ([CustomerID]
, [CustomerContactID]
, [PhoneNumber]
, [Extension]
, [PhoneTypeID]
, LastModifiedByStaffID)
VALUES (#CustomerID
, #CustomerContactID
, CASE WHEN COALESCE(#FormattedPhone, '')='' THEN NULL ELSE LTRIM(RTRIM(LEFT(dbo.RemoveNonNumeric(#FormattedPhone),10))) END
, CASE WHEN COALESCE(#FormattedPhone, '')='' THEN NULL ELSE CASE WHEN LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(#FormattedPhone),11,1000))) = '' THEN NULL ELSE LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(#FormattedPhone),11,1000))) END END
, #PhoneTypeID
, #StaffID)"
UpdateCommand="UPDATE [dbo].[CustomerPhones]
SET [CustomerContactID] = #CustomerContactID
, [PhoneNumber] = CASE WHEN COALESCE(#FormattedPhone, '')='' THEN NULL ELSE LTRIM(RTRIM(LEFT(dbo.RemoveNonNumeric(#FormattedPhone),10))) END
, [Extension] = CASE WHEN COALESCE(#FormattedPhone, '')='' THEN NULL ELSE CASE WHEN LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(#FormattedPhone),11,1000))) = '' THEN NULL ELSE LTRIM(RTRIM(SUBSTRING(dbo.RemoveNonNumeric(#FormattedPhone),11,1000))) END END
, [PhoneTypeID] = #PhoneTypeID
, [IsActive] = #IsActive
, [DateModified] = getdate()
, [LastModifiedByStaffID] = #StaffID
WHERE [CustomerPhoneID] = #CustomerPhoneID">
<SelectParameters>
<asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
</SelectParameters>
<DeleteParameters>
<asp:Parameter Name="CustomerPhoneID" Type="Int32" />
</DeleteParameters>
<UpdateParameters>
<asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
<asp:Parameter Name="FormattedPhone" Type="String" />
<asp:Parameter Name="PhoneTypeID" Type="Int32" />
<asp:Parameter Name="IsActive" Type="Boolean" />
<asp:SessionParameter Name="StaffID" Type="Int32" SessionField="StaffID" />
<asp:Parameter Name="CustomerPhoneID" Type="Int32" />
</UpdateParameters>
<InsertParameters>
<asp:ControlParameter Name="CustomerID" Type="Int32" ControlID="customerid" PropertyName="Text" />
<asp:ControlParameter Name="CustomerContactID" Type="Int32" ControlID="contactid" PropertyName="Text" />
<asp:Parameter Name="PhoneTypeID" Type="Int32" />
<asp:Parameter Name="FormattedPhone" Type="String" />
<asp:SessionParameter Name="StaffID" Type="Int32" SessionField="StaffID" />
</InsertParameters>
</asp:SqlDataSource>
<asp:Panel ID="phonesformcontent" runat="server" CssClass="formsection separate">
<section id="phonesform" class="simplelayoutform">
<h3>All Phones</h3>
<gcctl:MyCheckBox ID="chkPhoneShowInactive" Text="Show Inactive?" Checked="false" AutoPostBack="true" OnCheckedChanged="chkPhoneShowInactive_CheckedChanged" runat="server" />
<asp:label id="lblPhoneMessage" CssClass="responsemsg" runat="server" enableviewstate="False" />
<gcctl:gridviewextended ID="gvPhones" runat="server" DataSourceID="gvPhonesDataSource"
AutoGenerateColumns="False" DataKeyNames="CustomerPhoneID" EmptyDataText="No phones on record."
CssClass="searchresultsgrid" ShowFooter="True" OnRowCommand="gvPhones_RowCommand" AllowSorting="True"
ShowFooterWhenEmpty="true" OnRowDataBound="gvPhones_RowDataBound">
<Columns>
<asp:BoundField DataField="CustomerPhoneID" InsertVisible="false" ReadOnly="true" Visible="False" />
<asp:TemplateField HeaderText="Phone Type" SortExpression="PhoneType">
<FooterTemplate>
<asp:DropDownList ID="cboPhoneTypeID" runat="server"
DataSourceID="DataSourcePhoneTypes" DataTextField="PhoneType" DataValueField="PhoneTypeID"
SelectedValue='<%# Bind("PhoneTypeID") %>'>
</asp:DropDownList>
</FooterTemplate>
<EditItemTemplate>
<asp:DropDownList ID="cboPhoneTypeID" runat="server"
DataSourceID="DataSourcePhoneTypes" DataTextField="PhoneType" DataValueField="PhoneTypeID"
SelectedValue='<%# Bind("PhoneTypeID") %>'>
</asp:DropDownList>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="lblPhoneTypeID" runat="server" Text='<%# Bind("PhoneType") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Phone" SortExpression="PhoneNumber">
<FooterTemplate>
<asp:TextBox runat="server" Text='<%# Bind("FormattedPhone") %>' ID="txtPhone"></asp:TextBox>
</FooterTemplate>
<EditItemTemplate>
<asp:TextBox runat="server" Text='<%# Bind("FormattedPhone") %>' ID="txtPhone"></asp:TextBox>
</EditItemTemplate>
<ItemTemplate>
<asp:Label runat="server" Text='<%# Bind("FormattedPhone") %>' ID="lblPhone"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Active?" SortExpression="IsActive">
<FooterTemplate>
<asp:CheckBox runat="server" Checked='<%# Bind("IsActive") %>' ID="chkPhoneIsActive"></asp:CheckBox>
</FooterTemplate>
<EditItemTemplate>
<asp:CheckBox runat="server" Checked='<%# Bind("IsActive") %>' ID="chkPhoneIsActive"></asp:CheckBox>
</EditItemTemplate>
<ItemTemplate>
<asp:Label runat="server" Text='<%# Bind("IsActive") %>' ID="lblPhoneIsActive"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField ShowHeader="False">
<EditItemTemplate>
<asp:LinkButton runat="server" Text="Update" CommandName="Update" CausesValidation="True" ID="PhoneUpdate"></asp:LinkButton> <asp:LinkButton runat="server" Text="Cancel" CommandName="Cancel" CausesValidation="False" ID="PhoneEditCancel"></asp:LinkButton>
</EditItemTemplate>
<ItemTemplate>
<asp:LinkButton runat="server" Text="Edit" CommandName="Edit" CausesValidation="False" ID="PhoneEdit"></asp:LinkButton> <asp:LinkButton runat="server" Text="Delete" CommandName="Delete" CausesValidation="False" ID="PhoneDelete"></asp:LinkButton>
</ItemTemplate>
<FooterTemplate>
<asp:LinkButton runat="server" Text="Save New Phone" CommandName="FooterInsert" CausesValidation="True" ID="PhoneInsert"></asp:LinkButton>
</FooterTemplate>
</asp:TemplateField>
</Columns>
</gcctl:gridviewextended>
<div id="phonenotes" class="tip">
<div>NUMBERS ONLY - NO LETTER CODES IN THE PHONE FIELD!</div>
<div>Be sure to always enter the area code, especially if you're also adding an extension.</div>
<div>Note that only numbers will stay in the "Phone" field. Anything else you enter will disappear once it goes behind the scenes. The first 10 digits will become the phone number, and any remaining digits will become the extension.</div>
</div>
</section>
</asp:Panel>
<!-- End: Phones section -->
<div id="responsetextdiv" class="error"><asp:Literal ID="responsetext" runat="server"></asp:Literal></div>
</div>
<asp:XmlDataSource ID="DataSourcePhoneTypes" runat="server" DataFile="~/XML/PhoneTypes.xml" EnableCaching="true">
</asp:XmlDataSource>
</asp:Content>
Here's the code where I get the error:
protected void gvPhones_RowCommand(object sender, GridViewCommandEventArgs e)
{
// Insert data if the CommandName == "Insert"
// and the validation controls indicate valid data...
if (e.CommandName == "FooterInsert" && Page.IsValid)
{
//ERROR HAPPENS ON THE FOLLOWING LINE:
DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");
TextBox FormattedPhone = (TextBox)gvPhones.FooterRow.FindControl("txtPhone");
gvPhonesDataSource.InsertParameters["PhoneTypeID"].DefaultValue = PhoneTypeID.SelectedValue.ToString();
string sFormattedPhone = null;
if (!string.IsNullOrEmpty(FormattedPhone.Text))
sFormattedPhone = FormattedPhone.Text;
gvPhonesDataSource.InsertParameters["FormattedPhone"].DefaultValue = sFormattedPhone;
gvPhonesDataSource.InsertParameters["CustomerID"].DefaultValue = customerid.Text.ToString();
gvPhonesDataSource.InsertParameters["CustomerContactID"].DefaultValue = contactid.Text.ToString();
gvPhonesDataSource.InsertParameters["StaffID"].DefaultValue = System.Web.HttpContext.Current.Session["StaffID"].ToString();
// Insert new record
gvPhonesDataSource.Insert();
}
}
The full error I get is:
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Source Error:
Line 276: if (e.CommandName == "FooterInsert" && Page.IsValid)
Line 277: {
Line 278: DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");
Line 279: TextBox FormattedPhone = (TextBox)gvPhones.FooterRow.FindControl("txtPhone");
Line 280:
Source File: <snip> Line: 278
Stack Trace:
[NullReferenceException: Object reference not set to an instance of an object.]
GCWebForms.Directory.ContactEdit.gvPhones_RowCommand(Object sender, GridViewCommandEventArgs e) in <snip>ContactEdit.aspx.cs:278
System.Web.UI.WebControls.GridView.OnRowCommand(GridViewCommandEventArgs e) +137
System.Web.UI.WebControls.GridView.HandleEvent(EventArgs e, Boolean causesValidation, String validationGroup) +95
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +49
System.Web.UI.WebControls.GridViewRow.OnBubbleEvent(Object source, EventArgs e) +146
System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +49
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +5450
When stepping through (when trying to add a new row to a gridview that already has data in it), I found that gvPhones.FooterRow says that it's null. Again, this only happens if there is data in gvPhones. If the datatable is empty, then the footerrow insert code works without a hitch.
Any help would be greatly appreciated! :-)
EDIT: adding the relevant code behind Page_Load. I just added the DataBind() statement, but it didn't make a difference.
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
bool bolNewRec = (this.iContactID == null);
phonesformcontent.Visible = (!bolNewRec);
if (bolNewRec)
{ //snipping unrelated code
}
else
{
//snipping code that loads the data into the page
gvPhones.Sort("SortOrder, PhoneType", SortDirection.Ascending);
}
}
if (phonesformcontent.Visible)
gvPhones.DataBind();
}
...and, just in case, here's RowDataBound:
protected void gvPhones_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
DataRowView rowView = (DataRowView)e.Row.DataItem;
bool bolShowInactive = chkPhoneShowInactive.Checked;
if (!bolShowInactive && (Convert.ToBoolean(rowView["IsActive"]) == false))
e.Row.Visible = false;
else
e.Row.Visible = true;
rowView = null;
}
if (e.Row.RowType == DataControlRowType.Footer)
{
CheckBox chkIsActive = (CheckBox)e.Row.FindControl("chkPhoneIsActive");
chkIsActive.Checked = true;
chkIsActive = null;
}
}
Try using the sender in your code as below:
Replace this line:
DropDownList PhoneTypeID = (DropDownList)gvPhones.FooterRow.FindControl("cboPhoneTypeID");
For this:
DropDownList PhoneTypeID = (DropDownList)((GridView)sender).FooterRow.FindControl("cboPhoneTypeID");
Also, check the page load if the problem is not with the postback.
My answer is based on this question:
Unable to get gridview footer values in RowCommand
UPDATE:
Change your GridViewExtended class,
ShowFooterWhenEmpty property:
[Category("Behavior")]
[Themeable(true)]
[Bindable(BindableSupport.No)]
public bool ShowFooterWhenEmpty
{
get
{
if (this.ViewState["ShowFooterWhenEmpty"] == null)
{
this.ViewState["ShowFooterWhenEmpty"] = false;
}
return (bool)this.ViewState["ShowFooterWhenEmpty"];
}
set
{
this.ViewState["ShowFooterWhenEmpty"] = value;
}
}
GridViewRow:
private GridViewRow _footerRow;
public override GridViewRow FooterRow
{
get
{
GridViewRow f = base.FooterRow;
if (f != null)
return f;
else
return _footerRow;
}
}
I based my changes on this link:
Always show FooterTemplate, even no data
I wound up scrapping this entire class. Instead, I made regular asp:gridviews that are based on datasources that have union selects with one row with -1 in the key column (since all of my tables have single autoincrement PKs, no row will legitimately have -1 in the key column), and then put the following in RowDataBound:
if (e.Row.RowType == DataControlRowType.DataRow)
{
DataRowView rowView = (DataRowView)e.Row.DataItem;
string sKeyName = gvPhones.DataKeyNames[0].ToString();
if ((rowView[sKeyName].ToString() == "-1"))
e.Row.Visible = false;
else
e.Row.Visible = true;
rowView = null;
}
This hides any row with -1 in the key column. So there's always at least one row in the gridview (even if that one row is hidden), and the footer row always shows.

Delete command not firing on post back, asp.net repeater

I have a page that contains a data repeater which in turn contains a button that is meant to delete a delete a record from an SQL database. When this button is clicked I receive the following error: Procedure or function 'usp_setKS3_DeleteAssessment' expects parameter '#intAssessmentId', which was not supplied.
If I comment out the KS3_DataSource_Assessment.Delete(); line in the code behind the page loads without error and displays the message, but does not delete the record (which I expect as the line was commented out). I have no idea what to do to make this work... any ideas?
ASP.net
<asp:SqlDataSource ID="KS3_DataSource_Assessment" runat="server" ConnectionString="<%$ ConnectionStrings:sqlWriter %>" ProviderName="System.Data.SqlClient" SelectCommandType="StoredProcedure" SelectCommand="usp_getKS3_ManageAssessments" OnSelected="KS3_DataSource_Assessment_Selected" DeleteCommand="usp_setKS3_DeleteAssessment">
<SelectParameters>
<asp:ControlParameter ControlID="KS3_DropDown_ACYear" PropertyName="SelectedValue" Name="intACYearId" Type="Int32" />
<asp:ControlParameter ControlID="KS3_DropDown_Subject" PropertyName="SelectedValue" Name="intSubjectId" Type="Int32" />
<asp:ControlParameter ControlID="KS3_DropDown_Year" PropertyName="SelectedValue" Name="intYearGroup" Type="Int32" />
</SelectParameters>
<DeleteParameters>
<asp:Parameter Name="intAssessmentId" Type="Int32" />
</DeleteParameters>
</asp:SqlDataSource>
<asp:Repeater runat="server" ID="KS3_Repeater_Assessments" EnableViewState="true" DataSourceID="KS3_DataSource_Assessment" OnItemDataBound="KS3_Repeater_Assessments_ItemDataBound">
<ItemTemplate>
<!-- Assessments -->
<asp:Table runat="server" ID="KS3_Table_Assessments" CssClass="width100pc">
<asp:TableRow CssClass="SectionSubHeader">
<asp:TableCell><%# Eval("txtAssessmentTitle").ToString() %></asp:TableCell>
<asp:TableCell CssClass="width25px alignCenter alignVerticalMiddle" RowSpan="2"><asp:ImageButton SkinID="imgButtonDelete" runat="server" ID="KS3_Button_DeleteAssessment" OnCommand="KS3_Button_DeleteAssessment_Command" CommandArgument='<%# Eval("intAssessmentID").ToString() %>' OnClientClick="return confirm('Are you sure you want to delete this assessment?\n\nOnce deleted all data is lost.');" /></asp:TableCell>
<asp:TableCell CssClass="width25px alignCenter alignVerticalMiddle" RowSpan="2">Up</asp:TableCell>
<asp:TableCell CssClass="width25px alignCenter alignVerticalMiddle" RowSpan="2">Down</asp:TableCell>
</asp:TableRow>
<asp:TableRow CssClass="BackgroundBlack fontStyleItalic">
<asp:TableCell><%# Eval("txtAssessmentDescription").ToString() %></asp:TableCell>
</asp:TableRow>
</asp:Table>
<!-- Columns -->
<asp:SqlDataSource ID="KS3_DataSource_Columns" runat="server" ConnectionString="<%$ ConnectionStrings:sqlWriter %>" ProviderName="System.Data.SqlClient" SelectCommandType="StoredProcedure" SelectCommand="usp_getKS3_ManageAssessments">
<SelectParameters>
<asp:Parameter Name="intACYearId" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
</ItemTemplate>
</asp:Repeater>
Code Behind
protected void Page_Load(object sender, EventArgs e)
{
}
protected void KS3_Button_SelectSubject_Click(object sender, EventArgs e)
{
if (KS3_DropDown_Subject.SelectedIndex == 0) { DisplayMessage("Validation Error", "Please select a subject from the list below.", "error"); KS3_Panel_DataToManage.Visible = false; }
else if (KS3_DropDown_Year.SelectedIndex == 0) { DisplayMessage("Validation Error", "Please select a year group from the list below.", "error"); KS3_Panel_DataToManage.Visible = false; }
else
{
//No errors detected, start data display
Master.systemMessageHide();
KS3_Panel_DataToManage.Visible = true;
//Update display
KS3_Label_Title.Text = KS3_DropDown_Subject.SelectedItem.Text.Trim() + " - " + KS3_DropDown_Year.SelectedItem.Text.Trim();
//Bind the new data to the repeater
KS3_Repeater_Assessments.DataBind();
}
}
public void DisplayMessage(string title, string message, string type)
{
Master.systemMessageShow(title, message, type);
}
protected void KS3_DataSource_Assessment_Selected(object sender, SqlDataSourceStatusEventArgs e)
{
if (e.AffectedRows < 1) { KS3_Panel_NoAssessments.Visible = true; }
else { KS3_Panel_NoAssessments.Visible = false; }
}
protected void KS3_Button_DeleteAssessment_Command(object sender, CommandEventArgs e)
{
KS3_Label_Title.Text = "DataSubmitted";
KS3_DataSource_Assessment.DeleteParameters["intAssessmentId"].DefaultValue = e.CommandArgument.ToString();
Trace.Write("Item Value: " + e.CommandArgument.ToString());
KS3_DataSource_Assessment.Delete();
KS3_Repeater_Assessments.DataBind();
DisplayMessage("Assessment Delete", "The selected assessment has been deleted and all associated data has been removed.", "success");
}
It looks that you are missing this attribute of the data source control:
DeleteCommandType="StoredProcedure"
Default value is Text, which makes control think that the command provided is an SQL statement. Since you are using stored procedure, this is not the behavior you want.

insert variable into db in c# code behind using sqldatasource

I wanted to insert a variable into a table in db using the SqlDataSource. Below is my code in aspx:
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:ACM_DBConnectionString %>"
DeleteCommand="DELETE FROM [ContestRelation] WHERE [relationid] = #relationid"
InsertCommand="INSERT INTO [ContestRelation] ([contestid]) VALUES (#contestid)"
SelectCommand="SELECT [relationid], [contestid] FROM [ContestRelation]"
UpdateCommand="UPDATE [ContestRelation] SET [contestid] = #contestid WHERE [relationid] = #relationid">
<DeleteParameters>
<asp:Parameter Name="relationid" Type="Int32" />
</DeleteParameters>
<InsertParameters>
<asp:Parameter Name="contestid" Type="Int32" />
</InsertParameters>
<UpdateParameters>
<asp:Parameter Name="contestid" Type="Int32" />
<asp:Parameter Name="relationid" Type="Int32" />
</UpdateParameters>
</asp:SqlDataSource>
and the code behind:
protected void ConfirmContest_Click(object sender, EventArgs e)
{
try
{
foreach (GridItem item in RadGrid1.MasterTableView.Items)
{
GridDataItem dataitem = (GridDataItem)item;
TableCell cell = dataitem["selectContest"];
CheckBox checkBox = (CheckBox)cell.Controls[0];
if (checkBox.Checked)
{
string value = dataitem.GetDataKeyValue("contestid").ToString();
SqlDataSource1.InsertParameters.Add("contestid", value);
SqlDataSource1.Insert();
}
}
}
catch (Exception ex)
{
Response.Write("<script language='javascript'>alert('No data selected .');</script>");
}
}
The "value" variable is not inserted into the database table. Can anyone point me my mistakes? Thanks in advance.
Problem Solved!
I actually cannot add an existing insert parameter, instead, I change to following code
string value = dataitem.GetDataKeyValue("contestid").ToString();
SqlDataSource1.InsertParameters.Add("contestid", value);
SqlDataSource1.Insert();
to following code. And the problem is solved.
string value = dataitem.GetDataKeyValue("contestid").ToString();
SqlDataSource1.InsertParameters["contestid"].DefaultValue = value;
SqlDataSource1.Insert();

Retrieving Current Value from Telerik RadGrid EditFormItem

I have an ASP form created using Telerik's RadGrid control, already binding values from the database and displaying them, with the ability to add and delete entries from the table. I need to add editing functionality, but I've finally run into a block, and I can't find a way past it.
The ASP page contains both the RadGrid and the SqlDataSource I need to use:
<asp:SqlDataSource ID="ContactsSqlDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:DBConnectionString %>"
SelectCommand="etc..."
UpdateCommand="UPDATE Contacts
SET Salutation=#Salutation,
FirstName=#FirstName,
etc...
WHERE ClientContactID=#ClientContactID" CancelSelectOnNullParameter="False"
onupdating="ContactsSqlDataSource_Updating">
<UpdateParameters>
<asp:Parameter Name="Salutation" Type="String" />
<asp:Parameter Name="FirstName" Type="String" />
etc...
<asp:Parameter Name="ClientContactID" Type="String" />
</UpdateParameters>
<SelectParameters>
<asp:QueryStringParameter Name="GroupID" QueryStringField="ID" />
</SelectParameters>
</asp:SqlDataSource>
etc...
<telerik:RadGrid ID="ContactsRadGrid" runat="server"
AutoGenerateColumns="False" AutoGenerateDeleteColumn="True" CellSpacing="0"
DataSourceID="ContactsSqlDataSource" GridLines="None"
Skin="Windows7" onitemcommand="ContactsRadGrid_ItemCommand"
onitemdatabound="ContactsRadGrid_ItemDataBound"
AllowAutomaticUpdates="True">
<MasterTableView DataSourceID="ContactsSqlDataSource"
DataKeyNames="etc...">
<Columns>
etc...
</Columns>
<EditFormSettings EditFormType="Template">
<FormTemplate>
<asp:HiddenField ID="ClientIDHiddenField" runat="server"
Value='<%# Bind("ClientContactID") %>' />
etc...
<asp:TextBox ID="SalutationTextBox" runat="server" Text='<%# Bind("Salutation") %>' TabIndex="1" />
etc...
<asp:TextBox ID="FirstNameTextBox" runat="server" Text='<%# Bind("FirstName") %>' TabIndex="2" />
</FormTemplate>
</EditFormSettings>
</MasterTableView>
</telerik:RadGrid>
In my code behind, I have:
protected void ContactsRadGrid_ItemCommand(object sender, Telerik.Web.UI.GridCommandEventArgs e)
{
RadGrid grid = sender as RadGrid;
GridDataItem dataItem = e.Item as GridDataItem;
GridEditFormItem editItem = e.Item as GridEditFormItem;
if (e.CommandName == "Delete")
{
etc...
}
else if (e.CommandName == "Update")
{
ContactsSqlDataSource.Update();
}
}
protected void ContactsSqlDataSource_Updating(object sender, SqlDataSourceCommandEventArgs e)
{
GridEditFormItem editItem = (ContactsRadGrid.EditItems[0] as GridDataItem).EditFormItem;
string salutation = Convert.ToString(editItem.GetDataKeyValue("Salutation"));
string firstName = Convert.ToString(editItem.GetDataKeyValue("FirstName"));
etc...
string clientContactID = Convert.ToString(editItem.GetDataKeyValue("ClientContactID"));
e.Command.Parameters["#Salutation"].Value = salutation;
e.Command.Parameters["#FirstName"].Value = firstName;
etc...
e.Command.Parameters["#ClientContactID"].Value = clientContactID;
e.Command.Connection.Open();
e.Command.ExecuteNonQuery();
e.Command.Connection.Close();
}
This is where I finally got to in order to stop getting errors on the SQL execution, however there's one major problem: the values being pushed onto the parameters are the old values, the ones that were already in the database, not the values I've entered in the EditFormItem. If I hardcode something onto a parameter (eg, append ".com" onto email), the alteration is correctly reflected in both the database and the table after the "Save" button in the edit form is clicked. Without the manual edit, no changes occur, because the database is simply being updated with the values that were already present.
How can I get the current values in the edit form?
(I'm not opposed to getting this done in some way other than the DataSource_Updating event, that's just the solution I came up with which was closest to correct. I do need to stick with the RadGrid, though.)
I managed to find a resolution to this issue. In the first part of ContactsSqlDataSource_Updating, I now have:
string salutation = (editItem.FindControl("SalutationTextBox") as TextBox).Text;
string firstName = (editItem.FindControl("FirstNameTextBox") as TextBox).Text;
etc...
string clientContactID = (editItem.FindControl("ClientIDHiddenField") as HiddenField).Value;
The database now updates correctly.

ASP.NET Configure update button in GridView

I am using C# ASP.NET on VS2005.
I have a gridview table but it does not have a selection for Enable Editing when I right click on the Smart Tab.
Thus I manually added the edit button with the following code:
AutoGenerateEditButton="True"
The edit button has successfully appeared on my gridview like this:
When I click on the Edit button, the page is refreshed and the row is now editable:
However, when I pressed on the update button, I was brought to the error:
Updating is not supported by data source 'SqlDataSource1' unless UpdateCommand is specified.
http://i.stack.imgur.com/W97K0.png
I have no clue on how I can input or configure the UpdateCommand because I don't see any background code for the Update button.
Need help from experienced. Thank you in advance.
Edited: Added INSERT query in SqlDataSource1, however I still met the same error when I press the Update button.
You need to re-configure the SqlDataSource1 control though which you can add support for INSERT, DELETE, UPDATE along with SELECT.
Take a look at this tutorial.
while configurting sqldatasource when you configure the select statement for the gridview,there is a option as "advanced".click on that and then click on 'generate update,insert nad delete statements".
For example, try this out...
Firstly create a method to handle the update record.
private void UpdateOrAddNewRecord(string parametervalue1, string parametervalue2)
{
using (openconn())
{
string sqlStatement = string.Empty;
sqlStatement = "Update TableName set Name1 =#Name1 where Name2#Name2";
try
{
// SqlCommand cmd = new SqlCommand("storedprocedureName", con);
//cmd.CommandType = CommandType.StoredProcedure;
SqlCommand cmd = new SqlCommand(sqlStatement, con);
cmd.Parameters.AddWithValue("Name2", parametervalue2);
cmd.Parameters.AddWithValue("#Name1",parametervalue1);
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();
}
catch (System.Data.SqlClient.SqlException ex)
{
string msg = "Insert/Update Error:";
msg += ex.Message;
throw new Exception(msg);
}
finally
{
closeconn();
}
}
Now create the row updating method..
protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
string ParameterValue1 = ((TextBox)GridView1.Rows[e.RowIndex].Cells[0].Controls[0]).Text;
string ParameterValue2 = ((TextBox)GridView1.Rows[e.RowIndex].Cells[1].Controls[0]).Text; //Name
UpdateOrAddNewRecord(ParameterValue1, ParameterValue2); // call update method
GridView1.EditIndex = -1;
BindGridView();
Label2.Visible = true;
Label2.Text = "Row Updated";
}
Create Row Cancelling event..
protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
GridView1.EditIndex = -1; //swicth back to default mode
BindGridView();
}
Create row editing...
protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e)
{
GridView1.EditIndex = e.NewEditIndex;
BindGridView();
}
There are so many other way out to do this same activity in different fashion. This is most elementary way. Anyways if you find it useful, please mark it as your answer else let me know...
In your code I think you have not handled the event for "Update".
Have a look at the below example hope it might help you,
check for the "UpdateCommand".
Also write a Update event in C# to update.
<asp:DetailsView ID="ManageProducts" runat="server" AllowPaging="True"
AutoGenerateRows="False" DataKeyNames="ProductID"
DataSourceID="ManageProductsDataSource" EnableViewState="False">
<Fields>
<asp:BoundField DataField="ProductID" HeaderText="ProductID"
InsertVisible="False" ReadOnly="True" SortExpression="ProductID" />
<asp:BoundField DataField="ProductName" HeaderText="ProductName"
SortExpression="ProductName" />
<asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice"
SortExpression="UnitPrice" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Fields>
</asp:DetailsView>
<asp:SqlDataSource ID="ManageProductsDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
DeleteCommand=
"DELETE FROM [Products] WHERE [ProductID] = #ProductID"
InsertCommand=
"INSERT INTO [Products] ([ProductName], [UnitPrice], [Discontinued])
VALUES (#ProductName, #UnitPrice, #Discontinued)"
SelectCommand=
"SELECT [ProductID], [ProductName], [UnitPrice], [Discontinued]
FROM [Products]"
UpdateCommand=
"UPDATE [Products] SET [ProductName] = #ProductName,
[UnitPrice] = #UnitPrice, [Discontinued] = #Discontinued
WHERE [ProductID] = #ProductID">
<DeleteParameters>
<asp:Parameter Name="ProductID" Type="Int32" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="ProductName" Type="String" />
<asp:Parameter Name="UnitPrice" Type="Decimal" />
<asp:Parameter Name="Discontinued" Type="Boolean" />
<asp:Parameter Name="ProductID" Type="Int32" />
</UpdateParameters>
<InsertParameters>
<asp:Parameter Name="ProductName" Type="String" />
<asp:Parameter Name="UnitPrice" Type="Decimal" />
<asp:Parameter Name="Discontinued" Type="Boolean" />
</InsertParameters>
</asp:SqlDataSource>

Categories

Resources