Sunday, April 01, 2007

Server side controls and data binding in web parts

One of the most common mistakes (or questions) that developers encounter when starting with web part development is how to use server side controls such as buttons, data grids or drop-down boxes in a webpart.


This is because web part development is not the usual, easy (lazy) drag-and-drop development that most of us MS developers take for granted. Unless you use smartpart (which you shouldn't - article coming soon), there is no way to develop a web part in a drag-and-drop environment, and you are stuck with writing manual server side code.


The drag-and-drop environment usually takes care of the declaration of the objects we are drag-and-dropping, and the instantiation of the objects in the right time in the life cycle of the web page. With web parts, you don't have access to page events, just control (web part) events, and you have to know yourself how, when and where to do what with the server control you want to use in your web part.


This leads to several common mistakes:


  • Controls are rendered, but do not respond to events (you click on a button, the page posts-back but nothing happens)


  • Controls are rendered, but the data they are supposed to show is not showing


  • Controls are rendered with data in them, but if you change a property value in the web part, the data is not changed in the control until I refresh the page (or click "apply" twice)


All of these are easy to avoid, and should not baffle you as a developer. In this article I will show how to use a server side control, with data binding and event triggering in a web part.




My button is not triggering the click event


This is always for the same reason. You created the button in the wrong place in the web part life cycle.


Your code probably looks like this:


public class MyFirstWebPart : WebPart

{

    Label userName = 
new Label();

    Button changeUserButton = 
new Button(); //this is wrong!

    protected override void CreateChildControls()

    {

    

    userName.Text = 
"Ishai";

    changeUserButton.Text = 
"Change Label Text To 'Sagi'";

    changeUserButton.Click+=
new EventHandler(changeUserButton_Click);

    
this.Controls.Add(userName);

    
this.Controls.Add(changeUserButton);

    }

    
void changeUserButton_Click(object sender, EventArgs e)

    {

        userName.Text = 
"Sagi";

    }



}








Now - why doesn't this code work? you get a label with "Ishai", you get a button with some text, and when you click on the button the page does a post back but the label's text doesn't change. Basically the changeUserButton_Click event isn't getting triggered.
The answer is - the button is getting recreated every time you load the page. This is because the "= new Button();" instantiation is done in the wrong place. It is supposed to be done in the CreateChildControls event, so that the control doesnt get recreated every time the page is loaded.
If the control gets recreated, it looses its connection to the event handler function, and the event never gets triggered.

A bigger mistake I see often in the forums is when developers create the button in the web part in the render() event, which reconnects the control every time the web part is rendered.



The correct way to do the same thing is using the following code:



public class MyFirstWebPart : WebPart

{

    Label userName = 
new Label();

    Button changeUserButton;

    
protected override void CreateChildControls()

        {

    changeUserButton = 
new Button();

    userName.Text = 
"Ishai";

    changeUserButton.Text = "Change Label Text To 'Sagi'";

    changeUserButton.Click+=
new EventHandler(changeUserButton_Click);

    
this.Controls.Add(userName);

    
this.Controls.Add(changeUserButton);

        }

    
void changeUserButton_Click(object sender, EventArgs e)

    {

        userName.Text = 
"Sagi";

    }



}




Notice the difference(marked in bold)?

The button is instanciated in the CreateChildControls event, and will not get rebuilt each time the page is loaded. This means the click event will work when the button is clicked.



Controls are rendered, but data isn't rendered.


The major differance between the drag-and-drop interface and the code-your-control interface that we use for web parts, is that you don't get visual studio to automagically configure your data connection and binding for you. I know a lot of people who get really confused when tasked with using a data control manually, because they are not aware what should be done to connect the control to the data. They are used to doing it using the menus that come out of the box with visual studio when you design a page.

If you are such a developer, you need to learn the minimum of what automatic actions visual studio takes when you drag-and-drop a data control on the page, and then use menus to connect it. In a web part you do it manually.
The following example shows how to connect a data grid to a data table and show them on a web part. While the following code works, it is still not the best practice this article wants to point out - since it still has one little thing that can be done better - and avoid the last common mistake. But I will tackle that after I explain this code:


public class MyFirstWebPart : WebPart

{

    DataGrid grid;

    
protected override void CreateChildControls()

    {

        DataTable dt = 
null;



        
using (SPSite site = new SPSite(Page.Request.Url.ToString()))

        {

            
using (SPWeb web = site.OpenWeb())

            {

                SPList list = web.Lists[
"Contacts"];

                
string q = @"

                                <Where>

                                    <Gt>

                                        <FieldRef Name=""ID"" />

                                        <Value Type=""Counter"">0</Value>

                                    </Gt>

                                </Where>"
;

                SPQuery query = 
new SPQuery();

                query.Query = q;

                SPListItemCollection items = list.GetItems(query);

                dt = items.GetDataTable();

            }

        }





        grid = 
new DataGrid();

        grid.DataSource = dt;

        grid.AllowPaging = 
true;

        grid.AllowSorting = 
true;

        grid.GridLines = GridLines.None;

        grid.PageSize = 5;



        grid.PagerStyle.NextPageText = 
"Next";

        grid.PagerStyle.PrevPageText = 
"Previous";



        grid.AutoGenerateColumns = 
true;

        grid.DataBind();





        
this.Controls.Add(grid);





        
base.CreateChildControls();

    }





}






So, what is going on here?
  1. Like in the first example, most of my code is in the CreateChildControls event. As we are going to see later, this is not always the right place to put all of the code.


  2. Using the code from my commond coding tasks article, I connect to the sharepoint site that the web part is hosted in, and connect to the "Contacts" list.



  3. Using a simple CAML query to return all items in the contacts list (this is a sample on how to use queries), I get a datatable of all contacts in the list.
    (this is on the line that says dt = items.GetDataTable();)



  4. I create the datagrid object ("grid") in the createchildcontrols event. This is (as I mentioned before) the correct place to create server side controls.



  5. I set the datasource property of the DataGrid to point to the DataTable. This is where we define the connection between the two (this is what you used to do using a wizard in the drag-and-drop .net applications).
    I also set some default settings for the grid - like paging, sorting and that is should Auto Generate Columns. This is where I see a lot of mistakes in the forums - people assuming that just connecting the datasource to the datagrid and calling DataBind() should be enough to get something on the page, and then don't understand why there is nothing rendered. If you don't set AutoGenerateColumns to true, you will have to define (manually) which columns from the data table to display.



  6. Finally, I call the DataBind() method which is another thing people tend to forget. Without this call, you will not get information in the grid!.




Changing web part properties require me to click apply twice for the data to change!


This is a usual mistake that I have sinned my self in doing and has everything to do with where\when you get the data. If the data should change based on something that happens like changing the web part properties (or getting input from a connected web part), if you get the data before the change in the property is registered, then you will have to refresh the page to see the changes!


So how do we do it?

The answer may be complicated. It all depends on the control you are using and how you want to use it. In the following example I will still use the same datagrid code I used above, but will add a property to the web part to allow the user to specify which list to render. If I only add the property and change the code in the "SPList list = web.Lists["Contacts"];" line, the user will change the value of the property to another list, but still see the same values from the contacts list, until he refreshes the page. So I will move the code that builds the DataTable into the OnPreRender event.




public class MyFirstWebPart : WebPart

{

    DataGrid grid;

    DataTable dt = 
null;

    
private const string c_Default_List_Name = "Contacts";

    
private string listName = c_Default_List_Name;

    [WebBrowsable(
true),

    Personalizable(
true),

    Category(
"Web Part List Connection"),

    DisplayName(
"List Name"),

    WebDisplayName(
"List Name"),

    Description(
"Defines the list the web part will read from."),

    DefaultValue(c_Default_List_Name)]

    
public string ListName

    {

        
get { return listName; }

        
set { listName = value; }

    }



    
protected override void CreateChildControls()

    {

        grid = 
new DataGrid();



        grid.AllowPaging = 
true;

        grid.AllowSorting = 
true;

        grid.GridLines = GridLines.None;

        grid.PageSize = 5;



        grid.PagerStyle.NextPageText = 
"Next";

        grid.PagerStyle.PrevPageText = 
"Previous";



        grid.AutoGenerateColumns = 
true;



        
this.Controls.Add(grid);



        
base.CreateChildControls();

    }

    
protected override void OnPreRender(EventArgs e)

    {

        
using (SPSite site = new SPSite(Page.Request.Url.ToString()))

        {

            
using (SPWeb web = site.OpenWeb())

            {

                SPList list = web.Lists[
this.ListName];

                
string q = @"

                                <Where>

                                    <Gt>

                                        <FieldRef Name=""ID"" />

                                        <Value Type=""Counter"">0</Value>

                                    </Gt>

                                </Where>"
;

                SPQuery query = 
new SPQuery();

                query.Query = q;

                SPListItemCollection items = list.GetItems(query);

                dt = items.GetDataTable();

            }

        }

        grid.DataSource = dt;

        grid.DataBind();

        
base.OnPreRender(e);

    }

}





What is happening here?


The web part now has a property called ListName, which allows the user to select a different list to display in the grid.
The major change is that while
the DataGrid is still getting created and added to the controls collection in the CreateChildContorls event, which is where you should do that, it is only getting connected to the DataTable in the OnPreRender event - just before the page starts rendering the html. This means that changes made to the web part like setting the ListName property have already taken place.





Finally, although it is not part of this article's original scope, I will give a final example on how to specify to the web part which fields to display (since I know I will get questions on that).

Basically, what you need to do is to remove the code line that says AutoGenerateColumns = true and manually add columns. The problem is that columns may or may not have internal names. What I do is use the AutoGenerateColumns to see the field names, and then specify the column names as they are returned.

The following code sample will allow the user to select a list, and specify which fields to display:




    public class MyFirstWebPart : WebPart

    {

        DataGrid grid;

        DataTable dt = 
null;

        Label errLabel = 
new Label();

        #region properties

        #region ListName

        
private const string c_Default_List_Name = "Contacts";

        
private string listName = c_Default_List_Name;

        [WebBrowsable(
true),

        Personalizable(
true),

        Category(
"Web Part List Connection"),

        DisplayName(
"List Name"),

        WebDisplayName(
"List Name"),

        Description(
"Defines the list the web part will read from."),

        DefaultValue(c_Default_List_Name)]

        
public string ListName

        {

            
get { return listName; }

            
set { listName = value; }

        }

        #endregion



        #region
 AutoGenerateColumns

        
private const bool c_Default_AutoGenerateColumns = true;

        
private bool autoGenerateColumns = c_Default_AutoGenerateColumns;

        [WebBrowsable(
true),

        Personalizable(
true),

        Category(
"Web Part List Connection"),

        DisplayName(
"Show All Columns?"),

        WebDisplayName(
"Show All Columns?"),

        Description(
"Defines if the web part will display all the columns. If false, the columns have to be defined in the 'Columns To Display' property."),

        DefaultValue(c_Default_AutoGenerateColumns)]

        
public bool AutoGenerateColumns

        {

            
get { return autoGenerateColumns; }

            
set { autoGenerateColumns = value; }

        }

        #endregion



        #region
 ColumnsToDisplay

        
private const string c_Default_Columns_List = "";

        
private string columnsToDisplay = c_Default_Columns_List;

        [WebBrowsable(
true),

        Personalizable(
true),

        Category(
"Web Part List Connection"),

        DisplayName(
"Columns To Display (CSV)"),

        WebDisplayName(
"Columns To Display (CSV)"),

        Description(
"Defines a Comma Seperated Value list of the list's fields that will be displayed, if 'Show All Columns' is turned off."),

        DefaultValue(c_Default_Columns_List)]

        
public string ColumnsToDisplay

        {

            
get { return columnsToDisplay; }

            
set { columnsToDisplay = value; }

        }

        #endregion

        #endregion



        #region
 events

        
protected override void CreateChildControls()

        {



            grid = 
new DataGrid();



            grid.AllowPaging = 
true;

            grid.AllowSorting = 
true;

            grid.GridLines = GridLines.None;

            grid.PageSize = 5;



            grid.PagerStyle.NextPageText = 
"Next";

            grid.PagerStyle.PrevPageText = 
"Previous";



            
this.Controls.Add(grid);



            
base.CreateChildControls();

            errLabel.Text = 
"";

            
this.Controls.Add(errLabel);

        }

        
protected override void OnPreRender(EventArgs e)

        {

            errLabel.Text = 
"";

            
try

            {

                
using (SPSite site = new SPSite(Page.Request.Url.ToString()))

                {

                    
using (SPWeb web = site.OpenWeb())

                    {

                        SPList list = web.Lists[
this.ListName];

                        
string q = @"

                                <Where>

                                    <Gt>

                                        <FieldRef Name=""ID"" />

                                        <Value Type=""Counter"">0</Value>

                                    </Gt>

                                </Where>"
;

                        SPQuery query = 
new SPQuery();

                        query.Query = q;

                        SPListItemCollection items = list.GetItems(query);

                        dt = items.GetDataTable();

                        
if (this.AutoGenerateColumns)

                        {

                            grid.AutoGenerateColumns = 
true;

                        }

                        
else if (this.ColumnsToDisplay.Length > 0)

                        {

                            grid.AutoGenerateColumns = 
false;

                            
string[] columnNames = this.ColumnsToDisplay.Split(',');

                            
foreach (string columnName in columnNames)

                            {

                                
if (list.Fields.ContainsField(columnName))

                                {

                                    SPField field = GetField(list, columnName);

                                    
if (field != null)

                                    {

                                        System.Web.UI.WebControls.BoundColumn col = 
new BoundColumn();

                                        col.DataField = field.InternalName;

                                        col.HeaderText = field.Title;

                                        grid.Columns.Add(col);

                                    }

                                }

                            }

                        }

                    }

                }



                grid.DataSource = dt;

                grid.DataBind();

            }

            
catch (Exception ex)

            {

                errLabel.Text = ex.ToString();

            }

            
base.OnPreRender(e);

        }

        #endregion

        /// <summary>

        /// A function to get a field from a list, supports both internal names and display names

        /// </summary>

        /// <param name="list">The list that contains the field</param>

        /// <param name="name">The name of the field (internal or display names supported)</param>

        /// <returns>An SPField object if the field is found, null if not found.</returns>

        private SPField GetField(SPList list, string name)

        {

            SPField field = 
null;

            
try

            {

                field = list.Fields.GetFieldByInternalName(name);

            }

            
catch { }

            
try

            {

                field = list.Fields.GetField(name);

            }

            
catch { }

            
return field;

        }

    }







The final code has two more properties - allowing you to select if the web part should Auto Generate Columns, or, if not, which columns to display.
A custom function I wrote allows you to get a referance to the fields specified, even if the user used an internal name or a display name.
Finaly, I moved the column creating code into the OnPreRender event as well, so that if you change the properties and define columns, you dont have to refresh to see the changes.





I hope this answers any unanswered questions you may have had on how to use server controls and data binding in sharepoint web parts.

11 comments:

Scott said...

Excellent write-up and very helpful. Thanks for all your postings.

Popovic Sasa said...

Great article! Thank you for writing it and posting it so that we can use it.

Saurabh said...

hey I am doint the same thing as u wrote in ur blog....

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using System.Data;

namespace cd_WP {
public class cdWebpart : WebPart {
Label userName = new Label();
Button change;
protected override void CreateChildControls()
{
change = new Button();
userName.Text = "Saurabh";
change.Text = "Click";
change.Click += new EventHandler(change_Click);

this.Controls.Add(userName);
this.Controls.Add(change);
}

void change_Click(object sender, EventArgs e)
{
userName.Text = "My Name is Saurabh";
}
}
}

it doesn't work.....page is postback when i clicked to the button but label text remains same....not change in Label Text....whats wrong in this code......please tell me where m wrong

Ishai Sagi [SharePoint MVP] said...

Saurabh - you are doing the exact thing I am warning not to do: you are creating the label outside of createchildcontrols.

Instead of
Label userName = new Label();

do:

Label userName;

and in createchildcontrols:

protected override void CreateChildControls()
{
userName = new Label();
change = new Button();
userName.Text = "Saurabh";
this.Controls.Add(userName);
change.Text = "Click";
change.Click += new EventHandler(change_Click);

this.Controls.Add(userName);
this.Controls.Add(change);
}

Saurabh said...

Now i m doing the exact same thing....here is my code

namespace cd_WP {
public class cdWebpart : WebPart {
Label userName ;
Button change;
protected override void CreateChildControls()
{
userName = new Label();
change = new Button();
userName.Text = "CD";
change.Text = "Click";
change.Click += new EventHandler(change_Click);

this.Controls.Add(userName);
this.Controls.Add(change);
base.CreateChildControls();
}

void change_Click(object sender, EventArgs e)
{
userName.Text = "This is my First Custom Webpart in Sharepoint";
}
}
}


again doesn't work....label text is remain same.....

Ishai Sagi [SharePoint MVP] said...

That should work. Does the page postback? can you debug and see if the click event is getting called? can you change the words on the button so you can be sure the last version is deployed?

Madhur said...

very good writeup .. Should be included as FAQ in Sharepoint - Development and Programming Section ..

mohammad said...

Hi,
I used the paging but I faced a problem,
when I click on next page its working ,
but when I navigate back to the first page its not working (the PageIndexChanged event is not fired)
any ideas?

Malte said...

"A bigger mistake I see often in the forums is when developers create the button in the web part in the render() event, which reconnects the control every time the web part is rendered."

Is it then correct to asume that doing that inside a if !page isPostback inside the render, will do about the same thing?
Not that i want to do that, but im still stying to understand the way it works :)

Very nice post, it helped me understand ALOT :)

Anonymous said...

RE Saurabh,
your viewing the webpart in the webpart Gallery. You have to embed the webpart onto a site page for postback to execute.
Regards Zee Nadj




Anonymous said...

I am creating a webpart with data being displayed in gridview. I had the issue that if i change any properties in the toolpart and press ok still the old data is displayed until i refresh the page. I tried to put the binding part into the prerender method as you said and then i lost the ability to do paging. how to solve this issue.

regards