:::: MENU ::::

Wednesday, January 28, 2009

Daron Yondem provided a solution to refresh UpdatePanels from client side. He built an Extender control (with AjaxControlTookit) and use an invisible textbox as an trigger to raise an async postback. The extender model in ASP.NET AJAX is an light-weight solution to build extensions for existing controls, but these extenders based on AjaxControlToolkit is realy heavy. Using a single extender control build upon AjaxControlToolkit will cause all the base scripts to load on the page. It's probably not a good idea to use such a heavy model and some simple solution is just enough for some simple objectives.

All we should do for the current feature is just to generate JavaScript proxies in the page. Actually I've also build an simple control to help the devs to raise async postbacks from client side, encapsulated the way described in this post. It can be used as following:

<ajaxExt:JavaScriptUpdater ID="Updater1" runat="server" MethodName="Refresh">
   
<TriggeredPanels>
       
<ajaxExt:TriggeredPanel UpdatePanelID="up1" />
        <!-- other UpdatePanels need to be updated -->
   
</TriggeredPanels>
</ajaxExt:JavaScriptUpdater>

The MethodName property in this control indicates the name of the proxy method generated in the client. Each control will be provided a collection of references to the UpdatePanels on the page. And now, devs can raise an async postback to refresh those UpdatePanels by executing the following method:

UpdatePanels.Refresh();

First of all, we should define a class as the element for the collection of UpdatePanel would be triggered. It can't be easier:

public class TriggeredPanel
{
   
public string UpdatePanelID { get; set; }
}

Of course we'll expose the collection in the control:

[PersistChildren(false)]
[
ParseChildren(true)]
[
NonVisualControl]
public class JavaScriptUpdater : Control
{
    ...
 
   
private List<TriggeredPanel> m_triggeredPanels = new List<TriggeredPanel>();
 
    [
PersistenceMode(PersistenceMode.InnerProperty)]
   
public List<TriggeredPanel> TriggeredPanels
    {
       
get
        {
           
return m_triggeredPanels;
        }
    }
 
    ...
}

Please note that the MethodName can only be modified during initialize period. It's the way which has been widely used in ASP.NET AJAX to keep the consistency of an control's settings in the lifecycle. Please see the following code:

private bool m_pageInitialized = false;
 
private string m_methodName;
public string MethodName
{
   
get
    {
       
return this.m_methodName;
    }
   
set
    {
       
if (this.m_initialized)
        {
           
throw new InvalidOperationException("...");
        }
 
       
this.m_methodName = value;
    }
}
 
protected override void OnInit(EventArgs e)
{
   
base.OnInit(e);
 
   
this.Page.InitComplete += (sender, ev) =>
    {
       
this.m_pageInitialized = true;
    };
 
   
this.Page.Load += new EventHandler(OnPageLoad);
 
   
if (!ScriptManager.GetCurrent(this.Page).IsInAsyncPostBack)
    {
       
this.Page.PreRenderComplete += new EventHandler(OnPagePreRenderComplete);
    }
}

Each helper control on the page will put a invisible button on the page as the trigger. The following code will dynamically generate an LinkButton control on the page and handle its Click event:

private string m_triggerUniqueId = null;
 
private void OnPageLoad(object sender, EventArgs e)
{
   
LinkButton button = new LinkButton();
    button.Text =
"Update";
    button.ID =
this.ID + "_Trigger";
    button.Style[
HtmlTextWriterStyle.Display] = "none";
    button.Click +=
new EventHandler(OnTriggerClick);
 
   
this.Page.Form.Controls.Add(button);
 
   
this.m_triggerUniqueId = button.UniqueID;
   
ScriptManager.GetCurrent(this.Page).RegisterAsyncPostBackControl(button);
}

Before page's rendering, we should register the proxy to the page:

private static readonly string BaseScripts =
@"if (!window.UpdatePanels) window.UpdatePanels = {};
UpdatePanels.__createUpdateMethod = function(triggerUniqueId)
{
    return function()
    {
        __doPostBack(triggerUniqueId, '');
    }
}";
 
private const string RegisterMethodTemplate =
   
"\nUpdatePanels['{0}'] = UpdatePanels.__createUpdateMethod('{1}');";
 
private void OnPagePreRenderComplete(object sender, EventArgs e)
{
   
this.Page.ClientScript.RegisterClientScriptBlock(
       
this.GetType(),
       
"BasicScripts",
       
JavaScriptUpdater.BaseScripts,
       
true);
 
   
this.Page.ClientScript.RegisterClientScriptBlock(
       
this.GetType(),
       
this.m_triggerUniqueId,
       
String.Format(
           
JavaScriptUpdater.RegisterMethodTemplate,
           
this.MethodName,
           
this.m_triggerUniqueId),
       
true);
}

When the page received an async postback raised by the proxy, the Click event of the dynamic button would be fired. As you can see later, I defined three events in the contorl. The ResolveTriggeredPanel event is to help the control to find the specified UpdatePanels. In addition, the other two events,  Updating and Updated will be fired before and after refreshing the UpdatePanels:

public event EventHandler<ResolveTriggeredPanelEventArgs> ResolveTriggeredPanel;
public event EventHandler Updating;
public event EventHandler Updated;
 
private void OnTriggerClick(object sender, EventArgs e)
{
   
if (this.Updating != null)
    {
       
this.Updating(this, EventArgs.Empty);
    }
 
   
foreach (TriggeredPanel panel in this.TriggeredPanels)
    {
       
UpdatePanel updatePanel = this.FindTriggeredPanel(panel.UpdatePanelID);
       
if (updatePanel != null && updatePanel.UpdateMode != UpdatePanelUpdateMode.Always)
        {
            updatePanel.Update();
        }
    }
 
   
if (this.Updated != null)
    {
       
this.Updated(this, EventArgs.Empty);
    }
}
 
private UpdatePanel FindTriggeredPanel(string id)
{
   
UpdatePanel triggeredPanel = null;
 
   
if (id != null)
    {
        triggeredPanel =
this.NamingContainer.FindControl(id) as UpdatePanel;
    }
 
   
if (triggeredPanel == null)
    {
       
ResolveTriggeredPanelEventArgs e = new ResolveTriggeredPanelEventArgs(id);
       
if (this.ResolveTriggeredPanel != null)
        {
           
this.ResolveTriggeredPanel(this, e);
        }
 
        triggeredPanel = e.TriggeredPanel;
    }
 
   
return triggeredPanel;
}

Oh, I've missed the ResolveUpdatePanelEventArgs class. Here it is:

public class ResolveTriggeredPanelEventArgs : EventArgs
{
   
public string ID { get; private set; }
 
   
public UpdatePanel TriggeredPanel { get; set; }
 
   
public ResolveTriggeredPanelEventArgs(string id)
    {
       
this.ID = id;
    }
}

Here we have finished the control. Let's see a simple demo of it. The following is the declarative code in aspx file:

<asp:UpdatePanel runat="server" ID="up1">
   
<ContentTemplate>
        <%
= DateTime.Now.ToString() %>
   
</ContentTemplate>
</asp:UpdatePanel>
 
<ajaxExt:JavaScriptUpdater ID="Updater1" runat="server" MethodName="Refresh">
   
<TriggeredPanels>
       
<ajaxExt:TriggeredPanel UpdatePanelID="up1" />
   
</TriggeredPanels>
</ajaxExt:JavaScriptUpdater>
 
<input type="button" onclick="UpdatePanels.Refresh()" value="Refresh" />

When we click the button, the UpdatePanel will be refresh without updating the whole page. Let's see the proxy code generated in client side:

<script type="text/javascript">
   
if (!window.UpdatePanels) window.UpdatePanels = {};
   
    UpdatePanels._createUpdateMethod =
function(triggerUniqueId)
    {
       
return function()
        {
            $get(triggerUniqueId).click();
        }
    }
   
    UpdatePanels[
'Refresh'] = UpdatePanels._createUpdateMethod('Updater1_Trigger');
</script>
 
...
 
<a id="UpdaterButton" href="javascript:__doPostBack('UpdaterButton','')"
   
style="display:none;">Update</a>

The attachment is the source code of the JavaScriptUpdater control.

More

This example demonstrates three ways to compare two file listings:

·         By querying for a Boolean value that specifies whether the two file lists are identical.

·         By querying for the intersection to retrieve the files that are in both folders.

·         By querying for the set difference to retrieve the files that are in one folder but not the other.

Bb546137.alert_note(en-us,VS.90).gifNote:

The techniques shown here can be adapted to compare sequences of objects of any type.

The FileComparer class shown here demonstrates how to use a custom comparer class together with the Standard Query Operators. The class is not intended for use in real-world scenarios. It just uses the name and length in bytes of each file to determine whether the contents of each folder are identical or not. In a real-world scenario, you should modify this comparer to perform a more rigorous equality check.

http://i.msdn.microsoft.com/Global/Images/clear.gif Example

Visual Basic

http://i.msdn.microsoft.com/Global/Images/clear.gifCopy Code

Module CompareDirs
    Public Sub Main()
 
        ' Create two identical or different temporary folders 
        ' on a local drive and add files to them.
        ' Then set these file paths accordingly.
        Dim pathA As String = "C:\TestDir"
        Dim pathB As String = "C:\TestDir2"
 
        ' Take a snapshot of the file system.
        Dim list1 = GetFiles(pathA)
        Dim list2 = GetFiles(pathB)
 
        ' Create the FileCompare object we'll use in each query
        Dim myFileCompare As New FileCompare
 
        ' This query determines whether the two folders contain
        ' identical file lists, based on the custom file comparer
        ' that is defined in the FileCompare class.
        ' The query executes immediately because it returns a bool.
        Dim areIdentical As Boolean = list1.SequenceEqual(list2, myFileCompare)
        If areIdentical = True Then
            Console.WriteLine("The two folders are the same.")
        Else
            Console.WriteLine("The two folders are not the same.")
        End If
 
        ' Find common files in both folders. It produces a sequence and doesn't execute
        ' until the foreach statement.
        Dim queryCommonFiles = list1.Intersect(list2, myFileCompare)
 
        If queryCommonFiles.Count() > 0 Then
 
 
            Console.WriteLine("The following files are in both folders:")
            For Each fi As System.IO.FileInfo In queryCommonFiles
                Console.WriteLine(fi.FullName)
            Next
        Else
            Console.WriteLine("There are no common files in the two folders.")
        End If
 
        ' Find the set difference between the two folders.
        ' For this example we only check one way.
        Dim queryDirAOnly = list1.Except(list2, myFileCompare)
        Console.WriteLine("The following files are in dirA but not dirB:")
        For Each fi As System.IO.FileInfo In queryDirAOnly
            Console.WriteLine(fi.FullName)
        Next
 
        ' Keep the console window open in debug mode
        Console.WriteLine("Press any key to exit.")
        Console.ReadKey()
    End Sub
 
    ' This implementation defines a very simple comparison
    ' between two FileInfo objects. It only compares the name
    ' of the files being compared and their length in bytes.
    Public Class FileCompare
        Implements System.Collections.Generic.IEqualityComparer(Of System.IO.FileInfo)
 
        Public Function Equals1(ByVal x As System.IO.FileInfo, ByVal y As System.IO.FileInfo) _
            As Boolean Implements System.Collections.Generic.IEqualityComparer(Of System.IO.FileInfo).Equals
 
            If (x.Name = y.Name) And (x.Length = y.Length) Then
                Return True
            Else
                Return False
            End If
        End Function
 
        ' Return a hash that reflects the comparison criteria. According to the 
        ' rules for IEqualityComparer(Of T), if Equals is true, then the hash codes must
        ' also be equal. Because equality as defined here is a simple value equality, not
        ' reference identity, it is possible that two or more objects will produce the same
        ' hash code.
        Public Function GetHashCode1(ByVal fi As System.IO.FileInfo) _
            As Integer Implements System.Collections.Generic.IEqualityComparer(Of System.IO.FileInfo).GetHashCode
            Dim s As String = fi.Name & fi.Length
            Return s.GetHashCode()
        End Function
    End Class
 
    ' Function to retrieve a list of files. Note that this is a copy
    ' of the file information.
    Function GetFiles(ByVal root As String) As IEnumerable(Of System.IO.FileInfo)
        Return From file In My.Computer.FileSystem.GetFiles _
                  (root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
               Select New System.IO.FileInfo(file)
    End Function
End Module
 

C#

http://i.msdn.microsoft.com/Global/Images/clear.gifCopy Code

namespace QueryCompareTwoDirs
{
    class CompareDirs
    {
 
        static void Main(string[] args)
        {
            // Create two identical or different temporary folders 
            // on a local drive and change these file paths.
            string pathA = @"C:\TestDir";
            string pathB = @"C:\TestDir2";
 
            // Take a snapshot of the file system.
            IEnumerable<System.IO.FileInfo> list1 = GetFiles(pathA);
            IEnumerable<System.IO.FileInfo> list2 = GetFiles(pathB);
 
            //A custom file comparer defined below
            FileCompare myFileCompare = new FileCompare();
 
            // This query determines whether the two folders contain
            // identical file lists, based on the custom file comparer
            // that is defined in the FileCompare class.
            // The query executes immediately because it returns a bool.
            bool areIdentical = list1.SequenceEqual(list2, myFileCompare);
 
            if (areIdentical == true)
            {
                Console.WriteLine("the two folders are the same");
            }
            else
            {
                Console.WriteLine("The two folders are not the same");
            }
 
            // Find the common files. It produces a sequence and doesn't 
            // execute until the foreach statement.
            var queryCommonFiles = list1.Intersect(list2, myFileCompare);
 
            if (queryCommonFiles.Count() > 0)
            {
                Console.WriteLine("The following files are in both folders:");
                foreach (var v in queryCommonFiles)
                {
                    Console.WriteLine(v.FullName); //shows which items end up in result list
                }
            }
            else
            {
                Console.WriteLine("There are no common files in the two folders.");
            }
 
            // Find the set difference between the two folders.
            // For this example we only check one way.
            var queryList1Only = (from file in list1
                                  select file).Except(list2, myFileCompare);
 
            Console.WriteLine("The following files are in list1 but not list2:");
            foreach (var v in queryList1Only)
            {
                Console.WriteLine(v.FullName);
            }
 
            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }            
 
        // This method assumes that the application has discovery 
        // permissions for all folders under the specified path.
        static IEnumerable<System.IO.FileInfo> GetFiles(string path)
        {
            if (!System.IO.Directory.Exists(path))
                throw new System.IO.DirectoryNotFoundException();
 
            string[] fileNames = null;
            List<System.IO.FileInfo> files = new List<System.IO.FileInfo>();
 
            fileNames = System.IO.Directory.GetFiles(path, "*.*", System.IO.SearchOption.AllDirectories);            
            foreach (string name in fileNames)
            {
                files.Add(new System.IO.FileInfo(name));
            }
            return files;
        }
    }
 
    // This implementation defines a very simple comparison
    // between two FileInfo objects. It only compares the name
    // of the files being compared and their length in bytes.
    class FileCompare : System.Collections.Generic.IEqualityComparer<System.IO.FileInfo>
    {
        public FileCompare() { }
 
        public bool Equals(System.IO.FileInfo f1, System.IO.FileInfo f2)
        {
            return (f1.Name == f2.Name &&
                    f1.Length == f2.Length);
        }
 
        // Return a hash that reflects the comparison criteria. According to the 
        // rules for IEqualityComparer<T>, if Equals is true, then the hash codes must
        // also be equal. Because equality as defined here is a simple value equality, not
        // reference identity, it is possible that two or more objects will produce the same
        // hash code.
        public int GetHashCode(System.IO.FileInfo fi)
        {
            string s = String.Format("{0}{1}", fi.Name, fi.Length);
            return s.GetHashCode();
        }
    }
}
 

http://i.msdn.microsoft.com/Global/Images/clear.gif Compiling the Code

·         Create a Visual Studio project that targets the .NET Framework version 3.5. By default, the project has a reference to System.Core.dll and a using directive (C#) or Imports statement (Visual Basic) for the System.Linq namespace. In C# projects, add a using directive for the System.IO namespace.

·         Copy the code into your project.

·         Press F5 to compile and run the program.

·         Press any key to exit the console window.

http://i.msdn.microsoft.com/Global/Images/clear.gif Robust Programming

For intensive query operations over the contents of multiple types of documents and files, consider using the Windows Desktop Search engine.

http://i.msdn.microsoft.com/Global/Images/clear.gif See Also

Concepts

LINQ to Objects

LINQ and File Directories

 

This is a project I've created in CodePlex under Microsoft Public License (Ms-PL). You can find it here.

Simple Usage

Reflection is one of the most important features of .NET platform. The way of accessing/assigning a property or invoking a method dynamically is widely used by numerous projects. As we all know, invoke-by-reflection is much less efficient than direct access. FastReflectionLib provide the same as part of the refection features like executing method dynamically but give simple and faster implementations. It can be use as the foundation of reflection-based scenarios such as ORM framework.

Please look at the code snippets below:

using System;
using System.Reflection;
using FastReflectionLib;
 
namespace SimpleConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            PropertyInfo propertyInfo = typeof(string).GetProperty("Length");
            MethodInfo methodInfo = typeof(string).GetMethod("Contains");
 
            string s = "Hello World!";
 
            // get value by normal reflection
            int length1 = (int)propertyInfo.GetValue(s, null);
            // get value by the extension method from FastReflectionLib,
            // which is much faster
            int length2 = (int)propertyInfo.FastGetValue(s);
 
            // invoke by normal reflection
            bool result1 = (bool)methodInfo.Invoke(s, new object[] { "Hello" });
            // invoke by the extension method from FastReflectionLib,
            // which is much faster
            bool result2 = (bool)methodInfo.FastInvoke(s, new object[] { "Hello" });
        }
    }
}

When we get the MethodInfo object, we can call the Invoke method to execute the method. FastReflectionLib contains several extension methods on these types (such as MethodInfo), so we can just put "Fast" before the method to replace the build-in ones. Comparing with the build-in functions, these new implementations get big performance improvements.

Use the worker object directly

For example, the extension method "FastInvoke" defined for MethodInfo type gets the corresponding worker object by the MethodInfo instance as the key from cache, and execute the Invoke method in the worker object. Apparently keeping the worker object and use it directly after getting it would give us better perfermance than retrieving it from cache again and again.

Here's the list of worker types for each XxxInfo type:

  • PropertyInfo: IPropertyAccessor
  • MethodInfo: IMethodInvoker
  • ConstructorInfo: IConstructorInvoker
  • FieldInfo: IFieldAccessor

We can get an IMethodInvoker object from FastReflectionCaches.MethodInvokerCache by a MethodInfo object:

static void Execute(MethodInfo methodInfo, object instance, int times)
{ 
    IMethodInvoker invoker = FastReflectionCaches.MethodInvokerCache.Get(methodInfo);
    object[] parameters = new object[0];
    for (int i = 0; i < times; i++)
    {
        invoker.Invoke(instance, parameters);
    }
}

Default implementations of worker types and extensions

FastReflectionLib has already provide default implementations for each worker interfaces. These implementations generate lambda expressions and compile them (by call Compile method) to delegate objects with the same signatures as the corresponding reflection methods. The implementation is simle, general and safe than directly Emit and the performance is good enough for most scenarios (please refer to the Benchmarks section).

But it's not the most efficient way to do so. The best optimized way by Emit could get better performance than directly access the members in code (Dynamic Reflection Library is one of these implementations). You can build your one worker interface implementation and factory to replace the build-in ones if necessary:

public class BetterPropertyAccessor : IPropertyAccessor
{
    public BetterPropertyAccessor(PropertyInfo propertyInfo) { ... }
 
    ...
}
 
public class BetterPropertyAccessorFactory :
    IFastReflectionFactory<PropertyInfo, IPropertyAccessor>
{
    public IPropertyAccessor Create(PropertyInfo key)
    {
        return new BetterPropertyAccessor(key);
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        FastReflectionFactories.PropertyAccessorFactory =
            new BetterPropertyAccessorFactory();
 
        ...
    }
}

Default implementaions of caches and extensions

FastReflectionLib uses the caches based on System.Collections.Generic.Dictionary<TKey, TValue>. When we invoke a FastXxx extension method, the library would retrieve the worker object from the cache container and execute it. If the worker object cannot be found in the cache, a new one would be created by the corresponding factory. In the Benchmarks section we can found that considerable time is "wasted" in searching the cache.

If you have better cache implementation you can replace the build-in one as following:

public class BetterMethodInvokerCache :
    IFastReflectionCache<MethodInfo, IMethodInvoker>
{
    public IMethodInvoker Get(MethodInfo key) { ... }
}
 
class Program
{
    static void Main(string[] args)
    {
        FastReflectionCaches.MethodInvokerCache = 
            new BetterMethodInvokerCache();
 
        ...
    }
}

Cache the worker objects by your own

FastReflectionLib uses the reflection objects (such as PropertyInfo) as key to cache the worker object (such as PropertyAccessor). But in some scenarios, we can cache the worker objects by your own. Here's an sample web site "CustomCache" release with the source of FastReflectionLib, which contains a series of overloaded FastEval extension methods that can be used as an efficient replacement for the build-in Eval method in some cases. The sample:

  • uses both the Type of class and the property name as a "joint key" to cache the PropertyAccessor objects.
  • uses PropertyAccessor to get the value of property defined in an "anonymous object".
  • caches in the scope of "page" instead of the whole AppDomain. 

Benchmarks

A benchmark project is provided with the source of FastReflectionLib. Here's part of the results (the benchmarks were run on my laptop with release build):

Execute the following method 1,000,000 times:

public class Test
{
    public void MethodWithArgs(int a1, string a2) { }
}

The way to execute

Time elapsed (in second)

Directly invoke

0.0071397

Build-in reflection

1.4936181

MethodInvoker.Invoke

0.0468326

FastInvoke

0.1373712

 

More