In my previous post I have shown how we can use a C# function in the XSLT. Now I want to go one step further and use in the transformation the ASP.NET controls along with their events.
Let's start from the sample xml data:
<files>
<file Id="F58C2962-AC0D-4C55-80A8-79A724669F53" Name="file 1" Path="D:\Temp\" Extension="iso"/>
<file Id="FDC1358E-D9C8-4A70-ABE0-E0EF5E742E08" Name="file 2" Path="D:\Temp\" Extension="jpg"/>
</files>
I want to create a table from it and for each row I want to have a link which opens the Windows Explorer with this file selected. I have an ASP.NET Development Server so it will work. XSL for this xml looks as follow:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
xmlns:StringExtensions="urn:StringExtensions" xmlns:asp="remove"
>
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<table>
<tr>
<th>File name</th>
<th>Path</th>
<th>Extension</th>
</tr>
<xsl:for-each select ="files/file">
<tr>
<td>
<xsl:value-of select ="@Name"/>
</td>
<td>
<xsl:value-of select ="@Path"/>
</td>
<td>
<xsl:value-of select ="@Extension"/>
</td>
<td>
<asp:LinkButton runat="server" ID="{StringExtensions:Replace(@Id,'-','')}">Open</asp:LinkButton>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
I have used here a custom function to replace some characters. How to create that function is explained in my previous post. Link is in the first line of this post.
We can't just put an ASP.NET control in the stylesheet, because we will get an exception: System.Xml.XmlException: „asp” is not a declared prefix. To workaround it we must put in the stylesheet declaration new attribute: xmlns:asp="remove".
When we transform the xml data with this styleshet, next step is to create an ASP.NET control from html we got:
private Control CreateControl(string html)
{
html.Replace("xmlns:asp=\"remove\"", "");
return Page.ParseControl(html);
}
Now we can add this control to the placeholder control on our page.
The last what we have to do is to bind the click event to ours LinkButton's controls:
private void BindEvents()
{
var r = PlaceHolder1.FlattenChildren().OfType<LinkButton>().ToList();
r.ForEach(x =>
{
x.Click += LinkButton1_Click;
});
}
The FlattenChildren is an extension method which I found somewhere. It returns all the child control:
public static IEnumerable<Control> FlattenChildren(this Control control)
{
var children = control.Controls.Cast<Control>();
return children.SelectMany(c => FlattenChildren(c)).Concat(children);
}
Sample event can look like this:
protected void LinkButton1_Click(object sender, EventArgs e)
{
var elem = ((LinkButton)sender).ID;
Guid fileId = Guid.Parse(elem);
File selectedFile = DBSearchService.GetFileWithPathAndExtension(fileId);
Process.Start(new ProcessStartInfo("explorer", String.Format("/select, {0}\\{1}.{2}",
selectedFile.Path.Path,
selectedFile.Name, selectedFile.Extension.Name)));
}
Using Xslt along with XPath we can transform any xml document in the way we want and use it e.g. to create the html page. At this time, we have Xslt 2.0 and XPath 2, but unfortunatelly .Net Framework doesn't support them. We can only use Xslt 1.
We have two ways to work with Xslt 2. One is to use a 3rd party library, e.g. XQSharp or Saxon. The second option is to manually implement the missed in Xslt 1 functions. How to do it I want to show in this post.
First is the sample code. The goal is to create a table from this xml data:
<?xml version="1.0" encoding="utf-8" ?>
<books>
<book isAvailable="true" averageUsersRating="4.0">
<title>First title</title>
<author>First book author</author>
</book>
<book isAvailable="true" averageUsersRating="1.2">
<title>Second title</title>
<author>Second book author</author>
</book>
<book isAvailable="false" averageUsersRating="5.0">
<title>Third title</title>
<author>Third book author</author>
</book>
</books>
The books which are currently not available must be placed in red table row. And the books titles which have rating greater that 4 must be written in upper case.
Xsl to transform this data to html table looks as follow
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<table>
<tr>
<th>Title</th>
<th>Author</th>
<th>Average rating</th>
</tr>
<xsl:for-each select="books/book">
<xsl:choose>
<xsl:when test="@isAvailable='true'">
<tr class="bookIsAvailable">
<xsl:call-template name="bookRowTemplate"/>
</tr>
</xsl:when>
<xsl:otherwise>
<tr class="bookIsNotAvailable">
<xsl:call-template name="bookRowTemplate"/>
</tr>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
<xsl:template name="bookRowTemplate" >
<td>
<xsl:choose>
<xsl:when test="@averageUsersRating > 4">
<!-- return title in upper case -->
</xsl:when>
<xsl:otherwise>
<xsl:value-of select ="./title"/>
</xsl:otherwise>
</xsl:choose>
</td>
<td>
<xsl:value-of select ="./author"/>
</td>
<td>
<xsl:value-of select ="@averageUsersRating"/>
</td>
</xsl:template>
</xsl:stylesheet>
This stylesheet is I think self-explanatory. Colouring the appropriate table rows is easy epending on the value of isAvailable attribute.
The second requirement is to write the appropriate books titles in upper case. Xslt/XPath doesn't have (or I don't know) function for that, so we must write it manually.
In C# function this is easy to write:
public class XslStringExtensions
{
public string ConvertToUpperCase(string source)
{
return source.ToUpper(System.Globalization.CultureInfo.InvariantCulture);
}
}
But is it possible to use this C# method in the stylesheet? The answer is yes.
First we must add to the stylesheet new attribute: xmlns:StringExtensions="urn:StringExtensions". Now we can write in the xsl the code for executing this method:
<xsl:when test="@averageUsersRating > 4">
<xsl:value-of select ="StringExtensions:ConvertToUpperCase(./title)"/>
</xsl:when>
At the end, we must configure the XslCompiledTransform to use the custom C# function. To do that, we must create list with arguments:
XsltArgumentList args = new XsltArgumentList();
args.AddExtensionObject("urn:StringExtensions", new XslStringExtensions());'
And that list we must pass to the XslCompiledTransform.Transform object, e.g.
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(xslTemplate);
XmlDocument doc=loadedXmlDoc;
MemoryStream returnedHtml = new MemoryStream();
transform.Transform(doc.CreateNavigator(), args, returnedHtml);
NHibernate has many ways to create and executes queries. With one of them* - the Named Queries we can execute our manually created stored procedures. This example is dedicated to SQL Server. This is important to notice, because this code is database specified. That means, for each database the query can look diffrent. E.g. in SQL Server we call the stored procedure by using the syntax: exec <procedure>. In Oracle it is: call <procedure>, so we must remember it.
Let's assume we have a simple stored procedure in our database which takes two arguments:
create procedure FindFilesWithNameLike(@pattern nvarchar(max), @extension nvarchar(10)) as
select f.Id,f.Name,f.[Path],f.Size,f.Extension from [File] f
join Extension e on f.Extension=e.Id
where f.Name
like '%'+@pattern +'%'
and e.Name=@extension
To use it with NHibernate, we must create a mapping. This is done in the *.hbm.xml file (I didn't find if this can be done with Fluent NHibernate and/or mapping by code).
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<sql-query name="FindFilesWithNameLike" callable="true" >
<query-param name="pattern" type="System.String"/>
<query-param name="extension" type="System.String"/>
<return-scalar column ="Id" type ="System.Guid"/>
<return-scalar column ="Name" type ="System.String"/>
<return-scalar column ="Size" type ="System.Int64"/>
<return-scalar column ="Extension" type ="System.String"/>
<return-scalar column ="Path" type ="System.String"/>
exec FindFilesWithNameLike @pattern=:pattern, @extension=:extension
</sql-query>
</hibernate-mapping>
As you can see - nothing special. We are using the query-param to define the procedure parameters and return-scalars to define, what columns (with types) are returned. At the end there is our database-specified code to execute the stored procedure with parameters.
When we have a mapping, we can now execute this query:
public IEnumerable<File> Execute(string pattern, string extension)
{
return session.GetNamedQuery("FindFilesWithNameLike")
.SetParameter("pattern", pattern)
.SetParameter("extension", extension)
.SetResultTransformer(new ResultToFileTransformer())
.List<File>();
}
To do this, we are using the api for named queries. When we execute this query we will have a list of list of objects. Thats why I added the SetResultTransformer. When the database table and custom entity is mapped one to one, we can use the Transformers.AliasToBean<T>, otherwise we can create custom transformer. In this example it is the ResultToFileTransformer. That class must implement the IResultTransformer interface.
*probably with using the native sql support in the NHibernate this it possible either, but I didn't checked
I just wanted to return from sql server data as xml. This is very easy to do, because sql server has a build-in suppot for that.
select Id, Name from Extensions FOR XML RAW('Extension'), ROOT('Extensions')
But using this query, the returned column has name like XML_F52E2B61-18A1-11d1-B105-00805F49916B
It's been some time when I manually created a sql query, so I lost few minutes to think, how to alias this column using the AS keyword.
Correct query looks as follows
select (select Id,Name from Extensions FOR XML RAW('Extension') , ROOT('Extensions')) as alias
I see I just need to remember some basics about sql/tsql.. Too much NHibernate I see.
The WebBrowser control uses behind the scenes the IE engine to render the pages. Fortunatelly/Unfortunatelly is that, we have together with the rendered page the IE-default shortcuts, the context menu shown after pressing the right mouse button etc.
To disable e.g. refreshing the page by pressing F5, we can set the WebBrowserShortcutsEnabled property to false.
To disable the context menu, the WebBrowser control have either a property, called IsWebBrowserContextMenuEnabled. When we set it to false, the context won't be shown after pressing a right mouse button.
At this moment there is only one element to disable. This is when I press the Backspace button. I don't want to have the possibility to go to the previous page by pressing the button on the keybord. Unfortunatelly the control doesn't have a build-in property to disable this behavious, but we can do it by using some javascript code. In the page we can paste this code to do it:
document.onkeydown = function (e)
{
var evt = e || window.event;
if (evt)
{
var keyCode = evt.charCode || evt.keyCode;
if (keyCode === 8)
{
if (evt.preventDefault)
{
evt.preventDefault();
} else
{
evt.returnValue = false;
}
}
}
};
This code will be executed when the page will be loaded.
The WebBrowser control api lets us execute some js code either. If we, e.g. want to disable the backspace button only in certain circumstances, we can make a function from this code and execute it using the api by
webBrowser1.Document.InvokeScript("js function name to execute");
I'm not a desktop application developer. I wrote only one app using the WinForms, but I must tell, it was interesting experience. Now I'm writting second WinForms app and second time, I needed the possibility to updating the progress bar in an app.
So I think this is a good opportunity to put on the blog the code snippet for that to have a place where I can have some useful code for future references.
Ok, so let's go to the solution details. As every desktop application developer know (or should know), the WinForms controls can be accessed/property changed only from the same thread in which they were created. If we have an application and we try to change the progress bar value in some long-running algorithm and all works on the same thread, the program won't be responding to the moment that the algorithm will be over. This is not a situation we want. We want to have a responding gui for e.g. stopping the executing algorithm or simply we want to move the app window to other place on the screen. To have this possibility, the algorithm must be running in other thread and only the gui must be updating from the main thread.
In .NET Framework we have few choices to do that. We have BackgroundWorker class, Dispatcher or we can use Tasks from Task Parallel Library. In my first WinForms app I used the Tasks from TPL to do that so I will focus on this topic in this post.
In the code below is the example
public static File[] Search(SearchCriteria criteria, INotifyProgress notify)
{
notifyHandler = notify;
TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();
if (notify != null)
{
notify.MinValue = notify.CurrentValue = 0;
notify.MaxValue = criteria.Locations.ToList().Count;
}
notify.MaxValue = 1000;
for (int i = 1; i <= 1000; i++)
{
var task = Task.Factory.StartNew((variables) =>
{
//simulate some long running algorithm
Thread.Sleep(100);
return (int)variables; //return the iteration number
}, i);
task.ContinueWith(x =>
{
notify.CurrentValue = x.Result;
}, scheduler);
}
return null;
}
The for loop is important in this solution. As you can see, for each iteration there is creating a new Task which doing some long-running calculations. When algorithm is finished, the result is returned. Each Task has also a continuation, which is executed after the Task is finished. In this sample, in the continuation there is taken the Task result and this result is set to the progress bar Value property. As I told earlier, the controls can be changed only from the thread where they were created. This is why I used the TaskScheduler.FromCurrentSynchronizationContext() method and set is as a second parameter to the Task continuation. You probably see that, the main Task has a second parameter too. In this parameter we can provide any outer variables which we want to have access to inside the Task. The variables is an alias for our outer variables.
The INotifyProgress interface need one line of explanation too. This is a contract for the class which is a simple wrapper for the WinForms progress bar control. I used it, because I don't want to have reference from the business logic to the gui controls.
public interface INotifyProgress
{
int MaxValue { set; get; }
int MinValue { set; get; }
int CurrentValue { set; get; }
}
public class NotifyProgressBar : INotifyProgress
{
private ProgressBar progressBar;
public NotifyProgressBar(ProgressBar progressBar)
{
this.progressBar = progressBar;
}
public int MaxValue
{
get
{
return progressBar.Maximum;
}
set
{
progressBar.Maximum = value;
}
}
public int MinValue
{
get
{
return progressBar.Minimum;
}
set
{
progressBar.Minimum = value;
}
}
public int CurrentValue
{
get
{
return progressBar.Value;
}
set
{
progressBar.Value = value;
}
}
}
In my previous posts I have shown how to get xml data from database and deserialize it. But sometimes we want to work with the xml and do some operations on it. To working with xml, in the .net framework there are two tools for it. One is to use the XmlDocument class and second is the linq to xml with the XDocument class. I personally prefer to work with linq to xml, so I must convert the XmlDocument object to XDocument which I have after I took the xml from database.
To convert the XmlDocument to XDocument I have following extension methods
public static class XmlDocumentExtensions
{
public static XDocument ToXDocument(this XmlDocument document)
{
return document.ToXDocument(LoadOptions.None);
}
public static XDocument ToXDocument(this XmlDocument document, LoadOptions options)
{
using (XmlNodeReader reader = new XmlNodeReader(document))
{
return XDocument.Load(reader, options);
}
}
}
To convert back, that means from XDocument to XmlDocument I have following extension method
public static XmlDocument ToXmlDocument(this XDocument xDocument)
{
var xmlDocument = new XmlDocument();
using(var xmlReader = xDocument.CreateReader())
{
xmlDocument.Load(xmlReader);
}
return xmlDocument;
}
In one of my previous posts I have used my custom code to serialize and deserialized data to/from XmlDocument class.
Examples of use:
XmlDocument extensions=//some xml document taken from db
List<FileExtension> result =
SerializationUtils.DeSerializeXmlToObject<List<FileExtension>>(extensions);
List<FileExtension> extensions=//some new objects to save to database
XmlDocument serializedExtensions =
SerializationUtils.SerializeObjectToXml<List<FileExtension>>(extensions);
The full code for this snippets:
public static class SerializationUtils
{
public static T DeSerializeXmlToObject<T>(XmlDocument xmlDoc)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringReader reader = new StringReader(xmlDoc.InnerXml);
T result = (T)serializer.Deserialize(reader);
return result;
}
public static XmlDocument SerializeObjectToXml<T>(T obj)
{
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringWriter w = new StringWriter();
serializer.Serialize(w, obj);
XmlDocument result = new XmlDocument();
string xmlContent = w.ToString();
result.LoadXml(xmlContent);
return result;
}
}
In my previous post I have used a OperationsInTransaction.Execute() method. This is my simple wrapper for the NHibernate ISession and ITransaction method.
Normally when we want to execute some query in the transaction we must write this piece of code
using (ISession session = SessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
try
{
//some query
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
}
}
}
Unfortunatelly we must write it every time when we want to execute some query on the database. Following the DRY principle I have created mini wrapper for it:
public static class OperationsInTransaction
{
private static ISessionFactory sessionFactory;
private static ISessionFactory SessionFactory
{
get
{
if (sessionFactory == null)
{
sessionFactory = new NhibernateConfig().CreateFactory();
}
return sessionFactory;
}
}
public static void Execute(IsolationLevel level, Action<ISession> operations)
{
using (ISession session = SessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction(level))
{
try
{
operations(session);
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
}
public static void Execute(Action<ISession> operations)
{
Execute(IsolationLevel.ReadCommitted, operations);
}
}
In this piece of code I'm creating every time in the background the new ISession and ITransaction objects when I'm execute some queries.
With this wrapper, what I need to write is only:
OperationsInTransaction.Execute(session =>
{
var element = new SetAvailableExtensionsList().Execute(session, serializedExtensions);
session.Save(element);
});
I have a simple database table with only id and one xml column as shown below.

In this column I have a list of available extensions saved as xml.
public class FileExtensionsList
{
public FileExtensionsList()
{
Id = Guid.NewGuid();
}
public virtual Guid Id { set; get; }
public virtual XmlDocument Extensions { set; get; }
}
This xml I'm not creating manually. I have a custom entity which I serialize to xml:
[XmlRoot("FileExtensionsList")]
[Serializable]
public class FileExtension
{
[XmlAttribute]
public string Extension { set; get; }
public override bool Equals(object obj)
{
if (obj == null)
return false;
if ((obj as FileExtension) == null)
return false;
if (this.Extension == ((FileExtension)obj).Extension)
{
return true;
}
return false;
}
public override int GetHashCode()
{
return Extension.GetHashCode();
}
}
And now I want to get this list from db using NHibernate. I'm not sure, in NHibernate 3.2 maybe there is already a build-in solution for working with xml columns, but in this post I will show how we can use a custom user type. This solution work for me from month and I don't see a reason why not to use it.
At the beginning we must add this custom user type. I have found somewhere a working implementation. Unfortunatelly I don't remember where. Code is below:
/*
* UserType allowing easy saving of NHIbernate XmlDocument property.
*
* Example
* =======
*
* //Message.cs - Example class with a XmlDocument
* public class Message
* {
* public XmlDocument Body{get;set;}
* }
*
* //Message.hbm.xml - The mapping
* * * * *
*
* History
* =======
* - This code was found online somewhere, sorry, I can't remember where :-(
* - I've tweaked it a little to work with 2nd level cache and NHibernate 2.1.x.
* - Tobin Harris
*/
public class XmlType : IUserType
{
public new bool Equals(object x, object y)
{
if (x == null || y == null)
return false;
var xdoc_x = (XmlDocument)x;
var xdoc_y = (XmlDocument)y;
return xdoc_y.OuterXml == xdoc_x.OuterXml;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
if (names.Length != 1)
throw new InvalidOperationException("names array has more than one element. can't handle this!");
var document = new XmlDocument();
var val = rs[names[0]] as string;
if (val != null)
{
document.LoadXml(val);
return document;
}
return null;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = (DbParameter)cmd.Parameters[index];
if (value == null)
{
parameter.Value = DBNull.Value;
return;
}
parameter.Value = ((XmlDocument)value).OuterXml;
}
public object DeepCopy(object value)
{
var toCopy = value as XmlDocument;
if (toCopy == null)
return null;
var copy = new XmlDocument();
copy.LoadXml(toCopy.OuterXml);
return copy;
}
public object Replace(object original, object target, object owner)
{
throw new NotImplementedException();
}
public object Assemble(object cached, object owner)
{
var str = cached as string;
if (str != null)
{
var doc = new XmlDocument();
doc.LoadXml(str);
return doc;
}
else
{
return null;
}
}
public object Disassemble(object value)
{
var val = value as XmlDocument;
if (val != null)
{
return val.OuterXml;
}
else
{
return null;
}
}
public Types.SqlType[] SqlTypes
{
get
{
return new Types.SqlType[] { new SqlXmlType() };
}
}
public Type ReturnedType
{
get { return typeof(XmlDocument); }
}
public bool IsMutable
{
get { return true; }
}
}
public class SqlXmlType : Types.SqlType
{
public SqlXmlType()
: base(DbType.Xml)
{
}
}
public class SqlXmlStringType : Types.SqlType
{
public SqlXmlStringType()
: base(DbType.String, 4000)
{
}
}
How this code works is not important. Important is that, now we can map our entities using this code and get access to the xml columns in our tables.
So let's map the entity from the beginning of this post.
public class FileExtensionsListMap : ClassMapping<FileExtensionsList>
{
public FileExtensionsListMap()
{
Table("Extensions");
Id(x=>x.Id, x=>x.Generator(Generators.Guid));
Property(x => x.Extensions, x => x.Type<XmlType>());
}
}
I have used here the new to NHibernate 3.2 mapping by code, which I personally prefer much more than creating the *.hbm.xml files manually or even using the Fluent NHibernate. I used to have some problems with FN, maybe that is the reason..
Ok. When the mapping is created, we can configure the NHibernate and do some queries to check if everything works as we expecting. I'm not going to show right now hot to configure the NHibernate, this will be in one of my futures posts.
So let's look at the queries. To get the extensions as XMLDocument object we can use
public class GetAvailableExtensionsList
{
public XmlDocument Execute(ISession session)
{
ICriteria criteria = session.CreateCriteria<FileExtensionsList>()
.SetProjection(Projections.Property("Extensions"));
XmlDocument result = criteria.UniqueResult<XmlDocument>();
return result;
}
}
If we want to work on the objects, not the xml itself we can deserialized it:
SerializationUtils.DeSerializeXmlToObject<List<FileExtension>>(extensions)
where extensions is the XmlDocument object.
With the raw xml we can use e.g. XSL Transforations to convert it to html and show on the page.
To set the data to database, the query looks that
public class SetAvailableExtensionsList
{
public FileExtensionsList Execute(ISession session, XmlDocument extensions)
{
var element =
session.CreateCriteria<FileExtensionsList>()
.UniqueResult<FileExtensionsList>();
if (element == null)
{
element = new FileExtensionsList();
}
element.Extensions = extensions;
return element;
}
}
Now we must serialize the extensions and execute the ISession.Save() or ISession.Update() method on this query. ISession has the SaveOrUpdate() method too, but it need do something more to use it, because otherwise we will get an exception. In ony of my futures posts I will try to take a look at it.
In this example I'm using the ISession.Save() method
XmlDocument serializedExtensions =
SerializationUtils.SerializeObjectToXml<List<FileExtension>>(extensions);
OperationsInTransaction.Execute(x =>
{
var element = new SetAvailableExtensionsList().Execute(x, serializedExtensions);
x.Save(element);
});
What is the OperationsInTransaction and how the serialize/deserialize mechanism is implemented I will cover in other posts.
SQL Server has the possibility to e.g. query the xml in database with XPath, but I don't know yet if it is possible to do it with NHibernate. At this time I'm working with the xml after I took the entire file from the database in the c# code.
PS. This solution works properly with the xml column type in the database. But this is also possible to change the xml column type to nvarchar(max) and the solution will work in this situation too. To do it, we must change the code from
public Types.SqlType[] SqlTypes
{
get
{
return new Types.SqlType[] { new SqlXmlType() };
}
}
to
public Types.SqlType[] SqlTypes
{
get
{
return new Types.SqlType[] { new SqlXmlStringType() };
}
}
I'm an Asp.Net web developer, but the truth is, I always preferred the MVC than WebForms. Especially what I hate in WebForms is the update panel control. I have always problems with it when I want to use it in my current scenario, so easier to me is to write some jQuery code and do the controls refresh manually. I know that, there are situatios when I can't skip the update panels, so this post is a reference for my future battles.
A simple example of one update panel with button and some labels.
<asp:Label ID="Label4" runat="server" Text="Label4"></asp:Label>
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Label ID="Label3" runat="server" Text="Label3"></asp:Label>
<asp:Label ID="Label1" runat="server" Text="Label1"></asp:Label>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" EventName="click" />
</Triggers>
</asp:UpdatePanel>
protected void Page_Load(object sender, EventArgs e)
{
Label3.Text = Label4.Text = DateTime.Now.ToString();
}
protected void Button1_Click(object sender, EventArgs e)
{
Label3.Text = Label4.Text = DateTime.Now.ToString();
Label1.Text = "async post back";
}
In this snippet, the update panel is updated by pressing the button localed outside the UP, because this button is set as the async trigger.
How to set an asynchronous trigger to a control located on a custom user control.
This is the same sample as this one above. The only difference is that, the button is moved to custom user control. Because of that, the trigger in the update panel can't be set to this button, because in runtime we will get an exception saying that, the button is missing. Solution for this is set the async trigger manually in the code.
<asp:Label ID="Label4" runat="server" Text="Label"></asp:Label>
<uc1:WebUserControl1 ID="WebUserControl11" runat="server" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Label ID="Label3" runat="server" Text="Label"></asp:Label>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
</ContentTemplate>
</asp:UpdatePanel>
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Label3.Text = Label4.Text = DateTime.Now.ToString();
}
}
public partial class WebUserControl1 : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
ScriptManager.GetCurrent(this.Page).RegisterAsyncPostBackControl(Button1
);
}
protected void Button1_Click(object sender, EventArgs e)
{
((Label)Parent.FindControl("Label1")).Text = "async post back";
((Label)Parent.FindControl("Label3")).Text =
((Label)Parent.FindControl("Label4")).Text = DateTime.Now.ToString();
((UpdatePanel)Parent.FindControl("UpdatePanel1")).Update();
}
}
In this case, as I told earlier we must set the trigger manually. To do it, we need to get reference to ScriptManager located on our page and use the RegisterAsyncPostBackControl method with the button as a parameter. Thanks to this, we don't have any postback after pressing the button (which is outside UP). But in this time, there must be done one more thing. We get the result from the server with new values for controls (we can see it in e.g. firebug), but the controls on the page aren't updated. When we want to update any update panel in scenario like this one, the Update() method must be used as you can see in the code above.
How to refresh the nested update panel without touching the parent.
Update panels can be nested. Thanks to this feature, we can divide the page into small segments and update only one of them when we need, not the entire page. This is useful, because the server controls have a view state, which on page with many controls can have a huge size, what results in drop of performance in our application. Default, when we want to update only a nested UP, the parent is refreshed too, so we must to prevent ourselves from it. Let's add second UP to the previous example.
<asp:Label ID="Label4" runat="server" Text="Label"></asp:Label>
<asp:UpdatePanel ID="UpdatePanel2" runat="server" ChildrenAsTriggers="false" UpdateMode="Conditional">
<ContentTemplate>
<asp:Label ID="Label2" runat="server" Text="Label"></asp:Label>
<uc1:WebUserControl1 ID="WebUserControl11" runat="server" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:Label ID="Label3" runat="server" Text="Label"></asp:Label>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
</ContentTemplate>
</asp:UpdatePanel>
</ContentTemplate>
</asp:UpdatePanel>
In this scenario we have to add two additional attributes to the first UP. First we must set to false the ChildrenAsTriggers attribute. Thanks to it the update panel will skip on every postbacks from nested controls. The second attribute which we must apply, this is an UpdateMode attr set to Conditional.