Monday, November 30, 2009

Updating a resource file.

Recently one of our client had this requirement where they wanted to customize the texts appearing in the website as and when needed i.e. suppose they have a heading text in the login page which says “Login” and at some point they would like to change the text to something like “Login using email and password” without tinkering with the code. This is just an e.g., the client wanted each and every text appearing in the UI to be customizable.

The above requirement can be easily achieved using resource files. The text which are displayed can be placed in the resource files and whenever there is a change in the text just editing the resource file will take care of displaying the new text. Now you might be thinking what is the big deal in this, yes there is no rocket science involved in editing a resource file, just open any text editor and make the necessary modification. Asking the client to do that will be too much in the asking. As resource files are stored as XML, any syntax problem can bring down the whole application. So the next obvious solution is to provide a web page to edit the existing resource files.

By itself .NET doesn’t provide any class to edit and update resource files. There are classes to read and create resource files but nothing as such to update a resource files. If you want to simply read the content of a resource file you can make use of “System.Resources.ResourceReader” class and if you want to dynamically create a resource file then make use of “System.Resource.Resources.ResourceWriter” class. One of the disadvantages of using these classes is that they don’t have facility to fetch the comment part of a resource item.

My problem was not reading or creation of resource file but updating the content. Also I had to update the comments as well. So some googling helped me and gave me hints. People suggested that, treat a resource file as an XML and use the XML classes to read/update the resource file. With this hint and little bit of LINQ to XML I found the solution. Lets dive into the solution.

Reading resource file using LINQ

Following is the HTML content for displaying the resource file content.

<asp:GridView ID="gvResourceEditor" runat="server" AutoGenerateColumns="False" OnRowCancelingEdit="gvResourceEditor_RowCancelingEdit"
            OnRowEditing="gvResourceEditor_RowEditing" OnRowUpdating="gvResourceEditor_RowUpdating"            CellPadding="0" BorderWidth="0px" Width="100%" meta:resourcekey="gvResourceEditorResource1">
            <Columns>
                <asp:TemplateField HeaderText="Key" meta:resourcekey="TemplateFieldResource1">
                    <EditItemTemplate>
                        <asp:Label ID="lblKey" runat="server" Text='<%# Eval("Key") %>' meta:resourcekey="lblKeyResource1"></asp:Label>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="lblKey" runat="server" Text='<%# Eval("Key") %>' meta:resourcekey="lblKeyResource2"></asp:Label>
                    </ItemTemplate>
                    <ItemStyle CssClass="gridText tenXPadding bottomBorder rightBorder" Wrap="True" VerticalAlign="Top"
                        Width="10%" />
                </asp:TemplateField>
                <asp:TemplateField HeaderText="TEXT" meta:resourcekey="TemplateFieldResource2">
                    <HeaderStyle BorderWidth="0" BorderColor="White" BorderStyle="None" />
                    <EditItemTemplate>
                        <asp:TextBox ID="txtValue" TextMode="MultiLine" CssClass="textBoxEnabled" runat="server"
                            Text='<%# Bind("Value") %>' Width="100%" Height="150px" meta:resourcekey="txtValueResource1"></asp:TextBox>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="lblValue" runat="server" Text='<%# Bind("Value") %>' meta:resourcekey="lblValueResource1"></asp:Label>
                    </ItemTemplate>
                    <ItemStyle CssClass="gridText tenXPadding rightBorder bottomBorder" Wrap="True" Width="35%"
                        VerticalAlign="Top" />
                </asp:TemplateField>
                <asp:TemplateField HeaderText="COMMENTS" meta:resourcekey="TemplateFieldResource3">
                    <HeaderStyle BorderWidth="0" BorderColor="White" BorderStyle="None" />
                    <EditItemTemplate>
                        <asp:TextBox ID="txtComment" TextMode="MultiLine" CssClass="textBoxEnabled" runat="server"
                            Text='<%# Bind("Comment") %>' Width="100%" Height="150px" meta:resourcekey="txtDescriptionResource1"></asp:TextBox>
                    </EditItemTemplate>
                    <ItemTemplate>
                        <asp:Label ID="lblDescription" runat="server" Text='<%# Bind("Comment") %>' meta:resourcekey="lblDescriptionResource1"></asp:Label>
                    </ItemTemplate>
                    <ItemStyle CssClass="gridText tenXPadding rightBorder bottomBorder" Wrap="True" Width="35%"
                        VerticalAlign="Top" />
                </asp:TemplateField>
                <asp:CommandField ButtonType="Button" ItemStyle-CssClass="tenXPadding bottomBorder"
                    ShowEditButton="True" ControlStyle-CssClass="buttonEnabled" ItemStyle-VerticalAlign="Top"
                    HeaderStyle-BorderColor="White" HeaderStyle-BorderWidth="0" HeaderStyle-BorderStyle="None"
                    ItemStyle-Width="20%" meta:resourcekey="CommandFieldResource1">
                    <ControlStyle CssClass="buttonEnabled"></ControlStyle>
                    <HeaderStyle BorderColor="White" BorderWidth="0px" BorderStyle="None"></HeaderStyle>
                    <ItemStyle VerticalAlign="Top" CssClass="tenXPadding bottomBorder" Width="20%"></ItemStyle>
                </asp:CommandField>
            </Columns>
        </asp:GridView>

In the above code you can see there is a GridView control which has its columns bound to various properties like “Key”, “Value” and “Comment”. This is a simple grid with “Edit”, “Update” and “Cancel” facility. In the above HTML make sure you have the “Key” field. “Key” field is used to search the resource file for the record to be updated. Make sure the “Key” field is not editable.

In the Page_Load event in the code behind the following code is used to read the content of a resource file and data bind the same to a GridView control.

protected void Page_Load(object sender, EventArgs e)
{
   if (!IsPostBack)
     LoadData();
}

private void LoadData()
{
   string path = Server.MapPath("App_LocalResources/Default.aspx.resx");
   //Loading the XML file using the Load method of XElement.
   XElement resourceElements = XElement.Load(path);
   //LINQ to load the resource items
   gvResourceEditor.DataSource = (from resElem in resourceElements.Elements("data")
       select new
       {
           Key = resElem.Attribute("name").Value,
           Value = HttpUtility.HtmlEncode(resElem.Element("value").Value),
           Comment = resElem.Element("comment") != null ?
                HttpUtility.HtmlEncode(resElem.Element("comment").Value) : string.Empty
       });
    gvResourceEditor.DataBind();
}

In the above code you can see we are making use of XML to LINQ to read the contents from a resource file (Default.aspx.resx). Once we have loaded the resource file using the Load method of XElement class, in the next line we are making use of LINQ and creating anonymous types and assigning the same as DataSource to the GridView control.

With that simple code we have read the contents from a resource file and displayed the same in a GridView with facility to “Edit”, “Update” and “Cancel”. Now lets see how to update a particular value in a resource file.

Updating a resource file

In the “RowUpdating” event of the GridView control we are doing the following to update the resource file.

protected void gvResourceEditor_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    string theFileName = Server.MapPath("App_LocalResources/Default.aspx.resx");
    Label lblKey = gvResourceEditor.Rows[e.RowIndex].FindControl("lblKey") as Label;
    TextBox txtValue = gvResourceEditor.Rows[e.RowIndex].FindControl("txtValue") as TextBox;
    TextBox txtComment = gvResourceEditor.Rows[e.RowIndex].FindControl("txtComment") as TextBox;
    //Loading the Resource file as an XML.
    XDocument xDoc = XDocument.Load(theFileName);
    //Filtering out key which matches currently updated record.
    var elementToBeEdited = from xle in xDoc.Element("root").Elements("data") where xle.Attribute("name").Value == lblKey.Text select xle;

    if (elementToBeEdited != null && elementToBeEdited.Count() > 0)
    {
        //Updating the new value
        elementToBeEdited.First().SetElementValue("value", HttpUtility.HtmlDecode(txtValue.Text));

        //Checking if there is a comment node, if no then adding a new comment node.
        if (elementToBeEdited.First().Element("comment") == null && !string.IsNullOrEmpty(txtComment.Text))
        {
            elementToBeEdited.First().Add(new XElement("comment", HttpUtility.HtmlDecode(txtComment.Text)));
        }
        else if (elementToBeEdited.First().Element("comment") != null && !string.IsNullOrEmpty(txtComment.Text))
        {
            //If comment node is present then updating the comment node.
            elementToBeEdited.First().SetElementValue("comment", HttpUtility.HtmlDecode(txtComment.Text));
        }
        //Finally saving the Resource file.
        xDoc.Save(theFileName);               
    }

    gvResourceEditor.EditIndex = -1;
    LoadData();
}

In the above code, first we are finding controls holding the values and then we are loading the resource file into the XDoc object. Once the resource file is loaded, in the next line we are making use of LINQ statement to select the element whose name attribute matches the “Key” field of the GridView. Once the element to be edited is filtered next we are updating the “value” node with the newly updated value. The value node in the resource file holds the text.

Then in the next line we are checking whether there is a comment node available, if not we are creating a new comment node by calling the “Add” method and passing a new “XElement” object as the parameter. If there is a comment node then we are updating the comment by calling the “SetElementValue” method and passing the necessary text as argument. Finally the updated resource file is saved by calling the “Save” method of the XDoc class.

Isn’t it so simple to update the resource file. With XML to LINQ the code has become terse and very easy. So with this you have seen how to read and update a resource file using LINQ.

Note: If you are updating a resource file in a live system then keep in mind that all the logged in users session will expire and they will be logged out.

Try to konw more…

Sandeep

19 comments:

  1. Your post helped me a lot! I was just about to implement this idea and came across this link.

    ReplyDelete
  2. Its great to hear that my post helped you.

    ReplyDelete
  3. Hey Sandeep thanks for the post.for the ist time my edit btton is working bt for the second time it's redirection to my login page,can u tell me the reason?

    ReplyDelete
  4. Hi Sanju,
    The answer to your problem lies in the "Note" part of my blog. So if you have implemented forms authentication then editing of resource file will log out all the logged in users. So editing of the resource file is logging you out. Just exclude the page from forms authentication using the location tag.

    ReplyDelete
  5. Thanks Sandeep, reason behind is we'r modifying the resx file.Creating or modifying resx file at run time will exprired the session, m I right?Is there other solution bcoz my requirement is, admin will login n will mk some changes in the resx file which will be presented in the Gridview n then update.Please I dnt want to use any readymade editors.

    ReplyDelete
  6. Hi Sanju,
    Yes, perfectly right. I am not sure if there is any other way of doing this. But one suggestion which I can give is (not sure whether this will work out) that once the admin modifies a resx file and saves it, just before you call the function to save the resx retrieve the authentication cookie from the "Request" object and keep it. Once you have executed the save function, assign the authentication cookie to the current request. This way you are re-authenticating the expired session and admin will not get logged out. Hope this helps and please do keep posted on this.

    ReplyDelete
  7. Hello Sandeep, this far i come to the point that application gets restarted while updating resx file that means session of live users also gets expired which is not required.so m thinking of keeping the data in database n then bind that with resx file.trying for it wont know will succeed or not....
    Thanks nyways.

    ReplyDelete
  8. Hi Sanju,
    Yes, if you modify any resx files then all the logged in user' session will expire. "keeping the data in database n then bind that with resx file" if I am not wrong you are planning to keep your strings in database and whenever there is a change you are going to update the change to your resx files as well. If this is the case, then, here also you are modifying the resx files and all the live sessions will expire. Any modification of resx files will kill live sessions. Also resx files are not meant to be changed often. They get updated rarely, so you can ask the admin to update the resx files during odd times when you don't expect people to connect to your site.

    ReplyDelete
  9. Thanx Sandeep for the support.Can u tell me smthing abt the archtectures that u follow like 3-tier arc or n tier arc.How work flow goes on in it?

    ReplyDelete
  10. Hi,
    Silly question maybe, but when I compile my website I don't have anymore this App_GlobalResources folder or even can't find this resx files.
    How to update a resource then when the website is online?
    Thanks a lot

    ReplyDelete
  11. Hi Johanna,
    The question is perfectly valid one. When you compile a solution the resource files are also getting compiled and embedded into the dll. Open the properties window of the resource file and see the "Build Action" property. If you don't want your resx files to be embedded inside a dll change the "Build Action" property to "Content". Can you be more specific about "How to update a resource then when the website is online?"

    ReplyDelete
  12. hi Sandeep !

    That is really nice effort.but my scenario is a littile bit different.I do have two applications one is public and another Admin through which public sit is managed.From Admin i do manage the messages by updating resource.en-GB.resx file.And on public all the text of messages is fethed from resource file.Now the scenario is Suppose there is a user on public site.I do update resource from admin site.the user on public is logged out because session is flushed.Now on session_end event of global file i could not set cookies because i could not get response either request property on this event.Can you have any solution.I do use Web Application projects not web site.your scenario is you are updating resource file of same application so you can set cookies before updating resource but i couldn't........

    Reply

    ReplyDelete
    Replies
    1. Hi Khalil,
      You have got a tricky situation. The only solution I can think of is to redirect the user to your login page or some other page and try adding the AuthCookie into the response (without the user' knowledge). Once this is done redirect him back to the previous page.
      Please let me if this helps.

      Delete
  13. Nice but a small demo project that can be download would be nicer.

    ReplyDelete
  14. HiSandeep. Thanks for the great post. How do i change the "Build Action" to "Content" in VS2010. I don't want resx files to be embedded inside a dll.

    ReplyDelete
  15. Hello,

    Very useful post, thank you a lot. Do you anyhow recompile the whole project in order to regenerate the classes which correspond to each resx file?

    Thanks a lot.

    ReplyDelete
  16. Nice article.
    It's help me..
    Thanks for sharing it....

    ReplyDelete
  17. HI Sandeep,
    My Resource file is located in different class project. Then how can I set a path Change the file.

    Thanks,
    Bandara

    ReplyDelete

Please provide your valuable comments.