Feedback web part


A while back I got a question that how would I solve "feedback feature request" for my customers case (platform was MOSS 2007). Idea is that from every page of the portal you could go and give feedback. And if you do give feedback it would be important to know that what was the page that "initiated" the feedback for the user. And even more... if that page has page contact set then feedback would be automatically sent to the contact. Of course the feedback form should look a bit different dependending of the site where user decided to give feedback. Normally there would be a lot of possibilities to solve this kind of "feature request", but if you combine all the above... I'll only got one good solution so it's easy to choose what to write about this time 🙂

My solution (if you have better then let me know!)

So I decided to solve this by creating a web part. Web part could then host feedback form and have logic for the rest of the requirements. But in order to have easily changeable feedback form I thought that this web part could host user control that actually is the user interface (UI) for the form. And web part could runtime select the right form to use. So next I thought that I need to have some custom code to get the page contact from previous page and other handling code too. I didn't want to add code to the UI element since I needed to support multiple feedback forms. And if those forms are different from each other it would mean that I would need to get the values in runtime. Before going to the Visual Studio I decided to create small checklist:

  • Multiple feedback form UIs
    • No code to the UI!!!
    • Seperate the code and UI
  • Form should be easily changeable runtime
    • Decision making can't be restricted in any way and the solution could come from...
      • web part properties
      • users profile
      • site properties / some data from site
      • ...
  • Feedback should know the page that initiated the call

With that list I was ready to go! Of course I did the normal look up using search engines and found nice article that has something that I could use. It was User Control Container Web Part and it was about "SmartPart" which is web part that hosts user controls. So the basic groundwork was already set and I just started to work on the other features.

I used the Visual Studio extensions for WSS 3.0 and created new web part project. In just few clicks I had my project ready for testing. Here is the code I managed to do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Serialization;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;
using System.Web.UI.WebControls;
using System.Text;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Utilities;

namespace Feedback_Web_Part
{
  [Guid("f7bf59d1-f2be-4361-8a3e-f4604875fbcc")]
  public class Feedback_Web_Part : System.Web.UI.WebControls.WebParts.WebPart
  {
    // TODO: add property so that user can change the feedback form:
    String feedbackFormUrl = "/_controltemplates/Feedback.ascx";
    Control feedbackForm = null;
    HiddenField feedbackLocationUrl = null;
    
    public Feedback_Web_Part()
    {

    }

    protected override void CreateChildControls()
    {
      Controls.Clear();
      base.CreateChildControls();
      this.ChildControlsCreated = true;

      feedbackLocationUrl = new HiddenField();
      if (this.Page.IsPostBack == false)
      {
        feedbackLocationUrl.Value = this.Page.Request.ServerVariables["HTTP_REFERER"];
      }
      this.Controls.Add(feedbackLocationUrl);

      try
      {
        feedbackForm = this.Page.LoadControl(feedbackFormUrl);
        Button sendButton = feedbackForm.FindControl("SendFeedbackButton") as Button;
        if (sendButton != null)
        {
          sendButton.Click += new EventHandler(sendButton_Click);
        }
      }
      catch (System.Exception ex)
      {
        feedbackForm = new LiteralControl("Couldn't load '" + 
          feedbackFormUrl + "'! Error message: " + ex.Message);
      }

      if (feedbackForm != null)
      {
        // Add to the Controls collection to support postback 
        this.Controls.Add(feedbackForm);
      }
    }

    protected override void Render(HtmlTextWriter writer)
    {
      this.EnsureChildControls();
      this.RenderChildren(writer);

      if (feedbackForm.Visible == false)
      {
        // User has already submitted feedback:
        writer.Write("Thank you for your feedback!");
      }
    }

    void sendButton_Click(object sender, EventArgs e)
    {
      StringBuilder feedback = new StringBuilder(1024);
      if (feedbackForm != null)
      {
        foreach (Control control in feedbackForm.Controls)
        {
          if (control is TextBox)
          {
            TextBox textBox = control as TextBox;
            feedback.Append(textBox.ID.Replace("_", " ") + ": ");
            feedback.Append(textBox.Text);
            feedback.Append(Environment.NewLine + "<br/>");
          }
          else if (control is DropDownList)
          {
            DropDownList dropdown = control as DropDownList;
            feedback.Append(dropdown.ID.Replace("_", " ") + ": ");
            feedback.Append(dropdown.SelectedValue);
            feedback.Append(Environment.NewLine + "<br/>");
          }
          else if (control is CheckBoxList)
          {
            CheckBoxList checkBoxList = control as CheckBoxList;
            feedback.Append(checkBoxList.ID.Replace("_", " ") + ":<br/>");
            foreach (ListItem item in checkBoxList.Items)
            {
              if (item.Selected == true)
              {
                feedback.Append(item.Text + " ");
              }
            }
            feedback.Append("<br/>");
          }
        }
      }

      String referer = feedbackLocationUrl.Value;
      if (String.IsNullOrEmpty(referer) == false)
      {
        using (SPSite site = new SPSite(referer))
        {
          using (SPWeb web = site.OpenWeb("/"))
          {
            SPListItem item = web.GetListItem(referer);
            if (item != null)
            {
              if (PublishingPage.IsPublishingPage(item) == true)
              {
                PublishingPage publishingPage = PublishingPage.GetPublishingPage(item);
                // TODO: send email to local contact if set
                // => publishingPage.LocalContactEmail
                // OR
                // TODO: send email to contact if set
                // => publishingPage.Contact
                // OR whatever you like :-)
                SPListItem feedbackListItem = web.Lists["Feedback"].Items.Add();
                feedbackListItem[SPBuiltInFieldId.Title] = "Feedback from " + referer;
                feedbackListItem[SPBuiltInFieldId.AssignedTo] = publishingPage.Contact;
                feedbackListItem[SPBuiltInFieldId.Body] = feedback;
                feedbackListItem.Update();

                feedbackForm.Visible = false;
              }
            }
          }
        }
      }
    }
  }
}

And that code needs also Feedback.ascx to work:

You probably noticed that there isn't anything special about that user control. Only IDs of the controls have some special meaning. I highlighted "Send" buttons ID SendFeedbackButton because it's searched from my code (on line 46). Other IDs are used when data is pulled from the form to the feedback that is stored to the SharePoint task list. Between lines 82 and 111 I'll loop the controls and dynamically get the values to the feedback. And in order to make the feedback look more nice I'll replace underscores to spaces. You can see underscore in Feedback.ascx on line 7.

Feedback form in action

Well for the person who wants to give feedback the form looks like this (and again... just to remind you... I'm not UI designer :-):

And when user presses the Send-button the form renders like this:

So this is probably good place to explain the code on lines 36 - 41. It's used to store the HTTP_REFERER information to the page. Because if you don't store it and user presses the Send-key... you'll get the current page as referer and most likely that isn't what you want.

As always my code is fast and furious (maybe too fast sometimes?) and you should take it as example but fix all the problems that I'm ignoring 🙂 Like in your implementation you might want to use web part properties to control the usercontrol form. It's easy... just create property and remember to add [WebBrowsable(true)] on it. Then you're good to go.

Another important thing that I just "skipped"... is the user rights (if you DIDN'T notice it while reading the code... shame on you!!!). I just created that demo to run with the same user and that isn't probably what you want. You want probably to run that "Save" code in some other user account. For that you can use the following code:

1
2
3
4
5
6
7
8
9
10
 SPSecurity.RunWithElevatedPrivileges(delegate()
 {
   using (SPSite site = new SPSite(SPControl.GetContextSite(HttpContext.Current).ID))
   {
     using (SPWeb web = site.OpenWeb("/"))
     {
       // TODO: put the magic here!
     }
   }
 });

Feedback for the page contact

From code you can easily see that the task is created and it's assigned to the page contact. Here the email the page contact gets:

And if user clicks to see the item in SharePoint:

So it's pretty obvious for the page contact that he/she has received feedback regarding the page http://demo1/Pages/Main.aspx.

This could be also used for...

...sharing functionality with SharePoint apps and ASP.NET apps!

My title is kind of self describing 🙂 But if you want to have code that can be used in ordinary ASP.NET applications and have that same functionality in SharePoint too... well using the above methods it would be quite easily achievable. Just create web part that hosts you user controls that do some fancy stuff like get data from backend systems or whatever you need. Just keep in mind when creating the controls that it could be hosted in different kind of environments.

It think that this story has been told. Maybe next time something out of the SharePoint circle 🙂

Anyways... happy hacking!

J

Comments (18)

  1. anhpt says:

    Thank you very much for your article

  2. Nita Arvind says:

    Hi Jan,

    This article <http://blogs.msdn.com/jannemattila/archive/2007/06/07/feedback-web-part.aspx&gt; came to my rescue. There are a couple of differences... i have decided to try doing the same without a user control, but i am stuck at the point where i have to save changes to my XML file. The problem is that i dont know how to loop through the text boxes in the page. In the example you have presented you have used a control and therefore foreach (Control control in feedbackForm.Controls) helps, but in my case the page is an XSLT transformed, which i happen to be doing within the webpart.

    Do you have any suggestion as to how i can loop throught the controls in the webpart if i am not using a User control like you did?

  3. Hi Nita!

    I'm not sure if I really understood correctly but I'm still going to try to answer your question 🙂

    Yes you can search for controls within the control hierarchy with lots of different ways. If you know the names of the controls or if you know the level of the controls in the hierarchy. Or if you have some other attribute that identifies the controls.

    So you can loop controls hierarchy by creating recursive method that retrieves your control. I don't have that code in with me at this moment, but I'm trying to do it inline here 🙂

    Control FindMyControl(Control parent, String identifier)

    {

     foreach(Control c in parent.Controls)

     {

       // TODO: check for attribute or name or whatever you want

       if (c.Name.Equals(identifier))

       {

         return c;

       }

       Control controlFound = FindMyControl(c, identifier);

       if (controlFound != null)

       {

          return controlFound;

       }

     }

     return null;

    }

    So with that kind of code you can go through all the controls in your page (or web part or only some certain control).

    Probably the best way for you to continue is to start running your web part in debug and set breakpoints to correct locations and use immediate window (or other mechanisms) to see the control hiearachy in your case.

    Anyways... happy hacking!

    J

  4. zee says:

    liked the article

  5. samson says:

    need some information on html program

  6. Anil Kumar says:

    Thanks, for this article. It helped me a lot.

    Now, I am looking for something in WSS that can behave like Content query Webpart as CQWP is not a part of WSS and is available only in MOSS.  what can I do in order to get such a functionality in WSS? please suggest.

  7. Hi Anil!

    Unfortunately you need to write custom web part to handle all the stuff that CQWP does... it's not trivial task.

    You may want to check out that implementation with Reflector. It can be found in Microsoft.SharePoint.Publishing.dll and it's class is Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart. It's gives you pointers of SharePoint classes used to create the query.

    Anyways... Happy hacking!

    J

  8. News SharePoint and Non Profits an Awesome Mix Tools Feedback Web Part von Liam Cleary basierend auf

  9. News SharePoint and Non Profits an Awesome Mix Tools Feedback Web Part von Liam Cleary basierend auf

  10. Stephen E. Schuster says:

    Just curious,

    Your HTML code got skipped in a few section because of the beautiful Studio collapse/expand code feature. Do you have a fully expanded version of the acsx file?

    Thanks,

    Stephen

  11. Hi Stephen!

    Actually the acsx code isn't that interesting since it just contains a lot of html and just few controls. But here it goes anyway:

    <%@ Control Language="C#" ClassName="Feedback" %>

    <table>

       <tr>

           <td style="width: 79px">

               First name:</td>

           <td style="width: 230px">

               <asp:TextBox ID="First_Name" runat="server" Width="220px"></asp:TextBox></td>

       </tr>

       <tr>

           <td style="width: 79px">

               Last name:</td>

           <td style="width: 230px">

               <asp:TextBox ID="Last_Name" runat="server" Width="220px"></asp:TextBox></td>

       </tr>

       <tr>

           <td style="width: 79px">

               Email:</td>

           <td style="width: 230px">

               <asp:TextBox ID="Email" runat="server" Width="220px"></asp:TextBox></td>

       </tr>

       <tr>

           <td style="width: 79px">

               Phone:</td>

           <td style="width: 230px">

               <asp:TextBox ID="Phone" runat="server" Width="220px"></asp:TextBox></td>

       </tr>

       <tr>

           <td style="width: 79px">

               Age:</td>

           <td style="width: 230px">

               <asp:DropDownList ID="Age" runat="server" Width="220px">

                   <asp:ListItem>&lt; 18 years old</asp:ListItem>

                   <asp:ListItem>18 - 25</asp:ListItem>

                   <asp:ListItem>25 - 30</asp:ListItem>

                   <asp:ListItem>30 - 40</asp:ListItem>

                   <asp:ListItem>&gt; 40</asp:ListItem>

               </asp:DropDownList></td>

       </tr>

       <tr>

           <td colspan="2">

               I like to be contacted:</td>

       </tr>

       <tr>

           <td style="width: 79px">

           </td>

           <td style="width: 230px">

               <asp:CheckBoxList ID="Contact" runat="server" Width="220px">

                   <asp:ListItem>Phone</asp:ListItem>

                   <asp:ListItem>Email</asp:ListItem>

               </asp:CheckBoxList></td>

       </tr>

       <tr>

           <td colspan="2">

               My feedback:</td>

       </tr>

       <tr>

           <td colspan="2">

               <asp:TextBox ID="Feedback_Text" runat="server" Rows="8" TextMode="MultiLine" Width="299px"></asp:TextBox></td>

       </tr>

       <tr>

           <td style="width: 79px">

           </td>

           <td style="width: 230px">

               <asp:Button ID="SendFeedbackButton" runat="server" Text="Send" Width="220px" /></td>

       </tr>

    </table>

    Anyways... Happy hacking!

    J

  12. Hardik says:

    Great article.. tailor made for my need... Thanks Janne!

  13. Mehul b says:

    Hi Jane,

    Thanks for this wonderful article,

    Can you also provide the entire source-code download for this webpart project, it would be really helpfull...

  14. Tony says:

    Hi

    Is the source code available?

  15. Jussi Roine says:

    Janne, as always - this is good stuff and highly reusable 🙂

  16. Kurt says:

    Hi Janne,

    Thanks for the help WRT this web part.  I've successfully compiled the feedback web part in VS 2008, but when I push it to a demo SharePoint site, I don't see the feedback web part nor do I find it in  Site Features or Site Collection features to activate.  What am I missing here?  Thanks.

Skip to main content