Showing posts with label Web Part Development. Show all posts
Showing posts with label Web Part Development. Show all posts

Wednesday, August 04, 2010

Using jQuery validation in a sharepoint web part

As we all know, a sharepoint page can only have one <form > tag in it. As anyone who wanted to use jQuery validation knows, the validation script needs to run on a form tag.
So how do I use the validation code in a sharepoint web part?

The answer is to run the script against the form that already exists in the page. For example, I had to write a web part with an email address field, and wanted to help the users by validating the text they entered is a valid email address on the client side (I also validate on the server side - just in case someone is running a browser with no script). This is the "Render" override code that I used:

       txtEmailAddress1.CssClass = "required email";
       base.Render(writer);
       if (SPContext.Current.FormContext.FormMode != Microsoft.SharePoint.WebControls.SPControlMode.Edit)
       {
              writer.Write(@"<script>
$().ready(function() {
    $(""#" + this.Page.Form.ClientID + @""").validate();
});
</script>
");
       }

As you can see, I am adding a css class of type "required email" for the text box, and then telling the form on the page to validate using the jquery validation plug in. This of course assumes you added the references to the jquery scripts to the page...which you may want to do as part of the web part (override oninit, and registerclientscriptblock) or as part of the master page (if you expect a lot of web parts to use it).

As for the "if (SPContext.Current.FormContext.FormMode != Microsoft.SharePoint.WebControls.SPControlMode.Edit)" line - you have to be careful that your validation does not prevent you from editing the page. For example, if you remove that "if", you will not be able to change the properties of any web part on the page without first entering a valid email address in the textbox!

Sunday, June 20, 2010

SharePoint Conference Australia Presentation Notes

Thanks to everyone who came to see me and Brian present in the Australian SharePoint Conference last week!.
To answer the question that we got during the demo, the silverlight web part project template can be downloaded from this site: http://code.msdn.microsoft.com/vsixforsp.
Also, to see the code for Brian's twitter map web part, see his blog post on the subject: http://blog.brianfarnhill.com/2010/05/21/twitterbing-maps-web-part-how-i-did-it/.
Finally - for everyone who asked for the slide deck, it is now available from my company's (Extelligent Design) web site: http://www.extelligentdesign.com/in-the-news/sharepointconferenceaustraliapresentationnowavailable where you can also purchase KWizCom products (for australian\new Zealand customers only) or contact me about sharepoint training or consulting.

Tuesday, June 01, 2010

Validating Web Part Properties

Nothing is more annoying than configuring a web part by changing its properties and then hitting ok only to see the web part display an error that a property is invalid - and having to open the properties pane again to fix the problem.
To avoid this, best practice is to validate the data entered in the "set" of the web part property. For example, if I have a web part property that needs a comma delimited array of numbers (for example 1,2,3,4) and I don't want to build a tool part just for that, I can still build a property like this:

public string NumberArray
{
    get{return _numberArray;}
    set{_numberArray=value;}
}
The problem with the code above is that it is not validating that the string entered is indeed an array of numbers. To do that, I could change the code to something more like this:
public string NumberArray
{
    get{return _numberArray;}
    set{
          string [] arr = value.split(',');
          foreach (string item in arr)
          {
             int i;
             if(!int.TryParse(item,out i))
                throw new Exception("The item \""+item+"\" is not a valid number");
          }

_numberArray=value;}
}
This will do what I want - preven the user from closing the web part properties pane before fixing the error in the property, but it will not display the nice informative error to the user. Instead, it will show a generic error "An error has occurred" despite the fact that I specified what the error was when I threw the error!
Why? because to do that the exception must be of type WebPartPageUserException.
So the correct code for validating my sample property would be:
public string NumberArray
{
    get{return _numberArray;}
    set{
          string [] arr = value.split(',');
          foreach (string item in arr)
          {
             int i;
             if(!int.TryParse(item,out i))
                throw new WebPartPageUserException("The item \""+item+"\" is not a valid number");
          }

_numberArray=value;}
}

Now - when the users put an invalid value in my property they will be notified that it is invalid, and which value it was.

Monday, February 22, 2010

Making the content query web part deployable

My biggest issue with the CQWP? its not deployable when connected to a specific list in a specific site.
If you configure a CQWP web part to connect to a list in a site, the web part saves the ID (GUID) of the list - and if you want to deploy the web part as part of a feature or onet.xml so that the web part gets added every time a user creates a site, it will fail - because the ID of the list changes every time you create a new site.

My solution? override the web part and implement the following 2 functions:

public class MyBetterQueryWebPart: ContentByQueryWebPart
{
public void SetListGuid()
        {
            if (!string.IsNullOrEmpty(this.WebUrl) && 
!string.IsNullOrEmpty(this.ListName))
            {
                using (SPWeb web = SPContext.Current.Site.OpenWeb(this.WebUrl, true))
                {
                    SPList list = web.Lists[this.ListName];
                    this.ListGuid = list.ID.ToString();
 
                }
            }
 
        }
        protected override void OnLoad(EventArgs e)
        {
            if (!string.IsNullOrEmpty(this.WebUrl) && 
!string.IsNullOrEmpty(this.ListName) && 
string.IsNullOrEmpty(this.ListGuid))
            {
                SetListGuid();
            }
            base.OnLoad(e);
        }
}

This code checks if the web part is in list mode (has a list name and a web url) and is "corrupt" - doesnt have a list ID. if it finds that is the case, it dynamically loads the list ID based on the site url and the list name.

Wednesday, December 16, 2009

ErrorWebPart when loading a CQWP using the object model

When you are using the object model from a console or windows application to load web parts, you will notice that the CQWP (content query web part) instances all show up as "ErrorWebPart", and do not allow changing any of the web part's settings.

This is because when you load the web part, the web part code tries to access all kinds of data from it's context. Since you are loading the web part without a context (that is, from a windows application instead of a web application) the web part fails to get the context, and gets reported as an ErrorWebPart, when infact it works well if you look at it on the web interface.

I have yet to find a solution for this, but one workaround that works is to move your code to a web page, in the "_layouts" folder. This allows the code to run withing the site's context, and so accessing the web part does not cause it to report ErrorWebPart.

Update: It has been brought to my attention by a reader that Sandeep has a workaround for this:http://sharepoint-insight.blogspot.com/2008/10/sharepoint-all-webparts-appear-as.html

Sunday, July 13, 2008

Updating web part properties, without the tool pane

here is a nice developer article for you - something I see asked a lot of times, and Rob Nowik is answering with a great how to article: Updating web part properties, without the tool pane

Wednesday, October 17, 2007

Using the SharePoint date control in a web part


After a lot of requests in comments in this blog, I figured this must be very hard to implement. So I tried, and found it quite easy. Here is a code sample of a web part that uses the sharepoint date control



The code is simple - three controls - a DateTimeControl from the Microsoft.SharePoint.WebControls namespace, a button and a text box.
The button has a click event. On click, the event sets the text box to display the date that was selected in the DateTimeControl.





public class DatePickerWebPart : System.Web.UI.WebControls.WebParts.WebPart

    {

        DateTimeControl dtc;

        TextBox t;

        Button b;

 

        protected override void CreateChildControls()

        {

            base.CreateChildControls();

            dtc = new DateTimeControl();

            dtc.ID = "dtc" + this.UniqueID;

            this.Controls.Add(dtc);

            b = new Button();

            b.Text = "Click me to see the date";

            this.Controls.Add(b);

            b.Click += new EventHandler(b_Click);

            t = new TextBox();

            this.Controls.Add(t);

        }

 

        void b_Click(object sender, EventArgs e)

        {

            t.Text = dtc.SelectedDate.ToLongDateString();

        }

    }






Here is the result:

This is the web part when the page first loads






This is the web part when you click on the date picker






This is the web part when you click on the button




See? it is simple!

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.