ASP.NET | Dmytro Shteflyuk's Home https://kpumuk.info In my blog I'll try to describe about interesting technologies, my discovery in IT and some useful things about programming. Tue, 08 Sep 2015 00:01:45 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.1 fb:editor FBML tag in Facebook applications https://kpumuk.info/asp-net/fbeditor-editor-tag-in-facebook-applications/ https://kpumuk.info/asp-net/fbeditor-editor-tag-in-facebook-applications/#comments Wed, 16 Jan 2008 18:59:56 +0000 http://kpumuk.info/facebook/fbeditor-editor-tag-in-facebook-applications/ Some time ago I have started posting about Facebook Application Platform (see my posts about setFBML and Facebook libraries for .NET). Today’s topic is fb:editor. As you may see, Facebook has nice look and feel, and all applications usually adapted in some way to its interface. fb:editor FBML tag allows you to create forms which […]

The post fb:editor FBML tag in Facebook applications first appeared on Dmytro Shteflyuk's Home.]]>
Some time ago I have started posting about Facebook Application Platform (see my posts about setFBML and Facebook libraries for .NET). Today’s topic is fb:editor. As you may see, Facebook has nice look and feel, and all applications usually adapted in some way to its interface. fb:editor FBML tag allows you to create forms which looks just like native ones, but it has great limitation: it generates it’s own form tag, so can’t be used within ASP.NET server form. In this short post I’ll show HTML generated by fb:editor and a way how to use it in your ASP.NET application.

Here is example from fb:editor documentation:

fb:editor

First we need to include fb:editor tag to the page to force loading of CSS files:

1
<div style="display:none"><fb:editor /></div>

Now we should define basic structure of the form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<table class="editorkit" border="0" cellspacing="0" style="width:400px">
    <tr class="width_setter">
        <th style="width:50px"></th>
        <td></td>
    </tr>
    <tr>
        <th class="detached_label">
            <label for="login">Login:<br><small>(required)</small></label>
        </th>
        <td class="editorkit_row">
            <input name="login" id="login" />
        </td>
        <td class="right_padding"></td>
    </tr>
    <tr>
        <th></th>
        <td class="editorkit_buttonset">
            <input type="submit" class="editorkit_button action" value="Submit" />
        </td>
        <td class="right_padding"></td>
    </tr>
</table>

BTW, we have several enhancements in the original layout: we have specified for attribute for the label tag (now if you would click to the label, corresponding input would be selected), and added ability to mark required fields with required text (it’s impossible with fb:editor).

The post fb:editor FBML tag in Facebook applications first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/fbeditor-editor-tag-in-facebook-applications/feed/ 14
Coalesce ?? operator in C# 2.0 https://kpumuk.info/asp-net/coalesce-operator-in-c-sharp-2-0/ https://kpumuk.info/asp-net/coalesce-operator-in-c-sharp-2-0/#comments Mon, 14 Jan 2008 19:18:27 +0000 http://kpumuk.info/asp-net/coalesce-operator-in-c-sharp-2-0/ Operator ??, that was introduced in the .NET 2.0, takes first place in my top used C# idioms list a long time, but unfortunately it is rarely used in projects I’ve participated. Therefore these snippets could be found in production code very often: 12345public string Caption {     get { return ViewState["Caption"] != null […]

The post Coalesce ?? operator in C# 2.0 first appeared on Dmytro Shteflyuk's Home.]]>
Operator ??, that was introduced in the .NET 2.0, takes first place in my top used C# idioms list a long time, but unfortunately it is rarely used in projects I’ve participated. Therefore these snippets could be found in production code very often:

1
2
3
4
5
public string Caption
{
    get { return ViewState["Caption"] != null ? (string) ViewState["Caption"] : ""; }
    set { ViewState["Caption"] = value; }
}

Or even:

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
public string VisibleStatistic
{
    get
    {
        string retval = (string) ViewState["VisibleStatistic"];
        return retval == null ? "" : retval;
    }
    set
    {
        ViewState["VisibleStatistic"] = value;
    }
}

public string SelectedCategoryName
{
    get
    {
        object retval = ViewState["SelectedCategoryName"];
        if (retval != null)
            return (string) retval;
        return String.Empty;
    }
    set
    {
        ViewState["SelectedCategoryName"] = value;
    }
}

Almost 50% of this could be easily removed, and you will get beautiful and clear code, just right after you will understand what the hell is operator ??. Here is information from MSDN:

The ?? operator returns the left-hand operand if it is not null, or else it returns the right operand.

Easy and clean explanation. Let’s try to rewrite all these examples using this operator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public string Caption
{
    get { return (string) ViewState["Caption"] ?? String.Empty; }
    set { ViewState["Caption"] = value; }
}

public string VisibleStatistic
{
    get { return (string) ViewState["VisibleStatistic"] ?? String.Empty; }
    set { ViewState["VisibleStatistic"] = value; }
}

public string SelectedCategoryName
{
    get { return (string) ViewState["SelectedCategoryName"] ?? String.Empty; }
    set { ViewState["SelectedCategoryName"] = value; }
}

In case of value-types this approach will not work: you will get NullReferenceException. But do not worry — in this case we can use Nullable-types. Before:

1
2
3
4
5
public int FirstWidth
{
    get { return ViewState["FirstWidth"] != null ? (int) ViewState["FirstWidth"] : 0; }
    set { ViewState["FirstWidth"] = value; }
}

After:

1
2
3
4
5
public int FirstWidth
{
    get { return (int?) ViewState["FirstWidth"] ?? 0; }
    set { ViewState["FirstWidth"] = value; }
}

When you are using ?? operator, expression will be evaluated in the left to right order, so you can use something like this:

1
string name = FirstName ?? LastName ?? "Anonymous";

Of course, it looks more readably than the:

1
2
3
4
5
6
7
string name;
if (FirstName != null)
    name = FirstName;
else if (LastName != null)
    name = LastName;
else
    name = "Anonymous";

And a fortiori than:

1
2
3
string name = FirstName != null
        ? FirstName
        : (LastName != null ? LastName : "Anonymous");

Add it to your armoury!

The post Coalesce ?? operator in C# 2.0 first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/coalesce-operator-in-c-sharp-2-0/feed/ 18
Correct using Cache in ASP.NET https://kpumuk.info/asp-net/correct-using-cache-in-aspnet/ https://kpumuk.info/asp-net/correct-using-cache-in-aspnet/#comments Sat, 12 Jan 2008 19:22:00 +0000 http://kpumuk.info/asp-net/correct-using-cache-in-aspnet/ Often in ASP.NET application we see a code which looks like this one: 12345if (Cache["SomeData"] != null) {     string name = ((SomeClass) Cache["SomeData"]).Name;     //..... } Experienced developer, even if he is not a paranoiac, will find possible problem immediately — NullReferenceException. That’s because of caching implementation in ASP.NET. In ideal case […]

The post Correct using Cache in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
Often in ASP.NET application we see a code which looks like this one:

1
2
3
4
5
if (Cache["SomeData"] != null)
{
    string name = ((SomeClass) Cache["SomeData"]).Name;
    //.....
}

Experienced developer, even if he is not a paranoiac, will find possible problem immediately — NullReferenceException. That’s because of caching implementation in ASP.NET. In ideal case an object, that has been cached, will stay there up to application restart, but in real world it could be deleted between two calls: by the garbage collector when memory is over (because cache uses weak references WeakReference); by another thread to refresh cached data.

So the code I have mentioned before works in 99% of all cases, but sometimes you will get errors in your log, which can not be reproduced easily. Here is right cache usage approach:

1
2
3
4
5
6
SomeClass someClass = Cache["SomeData"] as SomeClass;
if (someClass != null)
{
    string name = someClass.Name;
    //.....
}

Do not relax your vigilance, it’s exactly what they are waiting for! (about bugs)

The post Correct using Cache in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/correct-using-cache-in-aspnet/feed/ 5
Crazy piece of code: Checking query parameter https://kpumuk.info/asp-net/crazy-piece-of-code-checking-query-parameter/ https://kpumuk.info/asp-net/crazy-piece-of-code-checking-query-parameter/#comments Mon, 17 Dec 2007 20:47:35 +0000 http://kpumuk.info/asp-net/crazy-piece-of-code-checking-query-parameter/ How often you are laughing out loud when looking on the someone’s code? Today I found great code in my current project and I can’t hold posting this to my blog. So, 1234567891011if (Request.QueryString.HasKeys()) {     string[] keys = Request.QueryString.AllKeys;     foreach (string k in keys)     {         […]

The post Crazy piece of code: Checking query parameter first appeared on Dmytro Shteflyuk's Home.]]>
How often you are laughing out loud when looking on the someone’s code? Today I found great code in my current project and I can’t hold posting this to my blog. So,

1
2
3
4
5
6
7
8
9
10
11
if (Request.QueryString.HasKeys())
{
    string[] keys = Request.QueryString.AllKeys;
    foreach (string k in keys)
    {
        if (k == "memberpagemode" && (string)Request.QueryString.GetValues(k).GetValue(0) == "edit")
        {
            pSett.ChangeFormViewMode(FormViewMode.Edit);
        }
    }
}

And how do you search through the hash for a key with specified value?

The post Crazy piece of code: Checking query parameter first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/crazy-piece-of-code-checking-query-parameter/feed/ 13
Generating content for the Facebook’s setFBML method in ASP.NET https://kpumuk.info/asp-net/generating-content-for-the-facebooks-setfbml-method-in-aspnet/ https://kpumuk.info/asp-net/generating-content-for-the-facebooks-setfbml-method-in-aspnet/#comments Thu, 13 Sep 2007 22:43:30 +0000 http://kpumuk.info/facebook/generating-content-for-the-facebooks-setfbml-method-in-aspnet/ In my current project we decided to build a Facebook application. This is really great platform with many interesting ideas inside, which usually means that you will spend a much time to make your application working as expected. Today I wanna talk about user profiles. Any Facebook application could add some action links, which will […]

The post Generating content for the Facebook’s setFBML method in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
In my current project we decided to build a Facebook application. This is really great platform with many interesting ideas inside, which usually means that you will spend a much time to make your application working as expected. Today I wanna talk about user profiles. Any Facebook application could add some action links, which will be displayed under the user’s picture, and some content for wide or narrow column. Of course, you can use FBML syntax, especially fb:if-... tags set to choose which content to show on specific profiles to concrete users.

For the beginning, I’ll post a few key points about user profiles. If you want to add some content to the profile of specific user, you should call profile.setFBML routine. For users that you have not called profile.setFBML for, the actions are read from the content in “Default FBML” section of your application settings. For the most part, this will apply to any user who has not added your application. What is “Default FBML” itself? If you have added application, you will see “Default FBML” on all profiles of users that you have not called profile.setFBML for, and it does not matter, if they have added your application or not (good place to put “Invite” link). The same behavior you would see, if you would call profile.setFBML for all users (and if you are crazy enough.)

Please note, “Default FBML” is cached indefinitely, so wait some time to get your content on profiles. Another thing — you can add only action links to user profiles, that have not added your application, and only your application user will see them. This is most important part of the documentation, and you should completely understand it. More detailed description could be found in the documentation for fb:profile-action tag and profile.setFBML routine.

FBML-content for user profiles

There are 4 profile-specific FBML tags exist:

  • fb:profile-action is used to add action links under the user picture.
  • fb:subtitle will be shown right under the title of the your application box.
  • fb:wide specifies content to be shown when your application box placed in wide column.
  • fb:narrow specifies content to be shown when your application box placed in narrow column.

Easy, right? So let’s examine these tags more precisely.

As I said early, fb:profile-action is used to place action links on the user’s profile. Usually you will add one link:

1
2
3
<fb:profile-action url="http://www.mysite.com/action/">
    Perform Action
</fb:profile-action>

What about following scenario: I want to see the link “View my products” when I’m looking my own profile, “View Taisia’s products”, if I’m looking profile of my wife Taisia (and she has added application too), and “Invite Roman to MyApp” when I’m looking profile of my friend Roman, and he has not added application. Here is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<fb:if-is-own-profile>
    <fb:profile-action url="http://apps.facebook.com/myapp/Products.aspx">View my products</fb:profile-action>
    <fb:else>
        <fb:if-is-app-user uid="profileowner">
            <fb:profile-action url="http://apps.facebook.com/myapp/Products.aspx">
                View <fb:name uid="profileowner" firstnameonly="true" possessive="true" /> products
            </fb:profile-action>
            <fb:else>
                <fb:profile-action url="http://apps.facebook.com/myapp/Invite.aspx">
                    Invite <fb:name uid="profileowner" firstnameonly="true" /> to MyApp
                </fb:profile-action>
            </fb:else>
        </fb:if-is-app-user>
    </fb:else>
</fb:if-is-own-profile>

Please note: this example is similar to example on the fb:profile-action documentation page, but has one big difference — it’s working: fb:if-is-app-user uid="profileowner" (in documentation example uid takes default value “loggedinuser”, but we need uid of the profile owner.)

This code will work completely only if you will put it into the “Default FBML” section of your application settings, or if you will call profile.setFBML for user, that has not added your application (but this is madness, what I have talked about early): I have specified action “Invite someone to MyApp” in section, which will be shown only on profile of user that has not added application.

BTW, do not forget to remove all line breaks before updating “Default FBML”, because Facebook replaces them with <br/>.

Another interesting thing you could see from working example: Facebook adds parameter id for all links in profile-action, and it equals to owner’s of the profile ID. In my previous example, if I will navigate to Roman’s profile, I will see hyperlink with URL http://apps.facebook.com/myapp/Invite.aspx?id=603839739. Great!

This example is simple, so let’s move ahead. We have two columns on the profile: left (narrow) and right (wide). You can specify in application settings which one will be default. To put content in wide column, use fb:wide, in narrow column — fb:narrow. Quite clear, right?

One more interesting issue. You can specify as many fb:wide and fb:narrow tags as you wish. All content, specified in fb:wide tags will be shown if you application box placed in wide column, all content from fb:narrow tags — when application box placed in narrow column. You can add content outside one of these tags, and it will be shown in both cases — when application in wide or narrow column. If no content specified for one of column, Facebook will show “No content to display.” text.

When to generate profile FBML?

Your application box on the user’s profile should reflect latest changes related to him. Facebook does not know when to update profile information, so you need to do it by yourself (anyway, only you as application developer know when something changes). So, usually you would call profile.setFBML after some changes in your application (for example, user or his friends added some data), depending on which information you are rendering on the profile. Sometimes it’s a good idea to set default data after user has added your application.

Creating profile FBML content with ASP.NET

For profile content generating I propose to use UserControls:

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
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ProfileFBML.ascx.cs" Inherits="ProfileFBML" %>
<fb:if-is-own-profile>
    <fb:profile-action url="http://apps.facebook.com/myapp/Products.aspx">View my products</fb:profile-action>
    <fb:else>
        <fb:if-is-app-user uid="profileowner">
            <fb:profile-action url="http://apps.facebook.com/myapp/Products.aspx">
                View <fb:name uid="profileowner" firstnameonly="true" possessive="true" /> products
            </fb:profile-action>
            <fb:else>
                <fb:profile-action url="http://apps.facebook.com/myapp/Invite.aspx">
                    Invite <fb:name uid="profileowner" firstnameonly="true" /> to MyApp
                </fb:profile-action>
            </fb:else>
        </fb:if-is-app-user>
    </fb:else>
</fb:if-is-own-profile>

<asp:XmlDataSource runat="server" ID="xdsCountries"
    DataFile="~/App_Data/CountryCodeList.xml" />

<fb:wide>
    <asp:Repeater runat="server" DataSourceID="xdsCountries" ID="rptCountriesWide">
        <ItemTemplate>
            <div>
                <%# XPath("CountryCoded") %> - <%# XPath("CountryName") %>
            </div>
        </ItemTemplate>
    </asp:Repeater>
</fb:wide>

<fb:narrow>
    <asp:Repeater runat="server" DataSourceID="xdsCountries" ID="rptCountriesNarrow">
        <ItemTemplate>
            <div>
                <%# XPath("CountryCoded") %>
            </div>
        </ItemTemplate>
    </asp:Repeater>
</fb:narrow>

How to get string to put it into the profile.setFBML? Here is the code:

1
2
3
4
5
6
7
8
StringBuilder sb = new StringBuilder();
StringWriter tw = new StringWriter(sb);
HtmlTextWriter hw = new HtmlTextWriter(tw);
Control c = LoadControl("~/ProfileFBML.ascx");
Controls.Add(c);
c.RenderControl(hw);
Controls.Remove(c);
string fbml = sb.ToString();

I’ve added control to the Controls collection to get events fired. If you would call this snippet from OnLoad, only OnInit would be fired in ProfileFBML, so do not forget to call DataBind method to force data binding from OnInit in this case.

The post Generating content for the Facebook’s setFBML method in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/generating-content-for-the-facebooks-setfbml-method-in-aspnet/feed/ 18
Synchronous page method call in ASP.NET AJAX library https://kpumuk.info/asp-net/synchronous-page-method-call-in-asp-net-ajax-library/ https://kpumuk.info/asp-net/synchronous-page-method-call-in-asp-net-ajax-library/#comments Mon, 10 Sep 2007 21:15:28 +0000 http://kpumuk.info/asp-net/synchronous-page-method-call-in-asp-net-ajax-library/ Sometimes we need to do some tasks, that libraries developers even have not foreseen. One of such cases is to make synchronous AJAX call (Asynchronous JavaScript And XML). Below you could find quick solution. Please note: my solution is just copy/past of the XmlHttpExecutor code with two small changes: I have passed false as third […]

The post Synchronous page method call in ASP.NET AJAX library first appeared on Dmytro Shteflyuk's Home.]]>
Sometimes we need to do some tasks, that libraries developers even have not foreseen. One of such cases is to make synchronous AJAX call (Asynchronous JavaScript And XML). Below you could find quick solution.

Please note: my solution is just copy/past of the XmlHttpExecutor code with two small changes:

  • I have passed false as third parameter of the XMLHttpRequest.open method to make synchronous call.
  • I have cached request results in XMLHttpSyncExecutor members before freeing XMLHttpRequest in the _onReadyStateChange function.

So let’s get started. First of all we would define XMLHttpSyncExecutor class, descendant of the WebRequestExecutor.

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
Type.registerNamespace('Sys.Net');

Sys.Net.XMLHttpSyncExecutor = function()
{
    if (arguments.length !== 0) throw Error.parameterCount();
    Sys.Net.XMLHttpSyncExecutor.initializeBase(this);

    var _this = this;
    this._xmlHttpRequest = null;
    this._webRequest = null;
    this._responseAvailable = false;
    this._timedOut = false;
    this._timer = null;
    this._aborted = false;
    this._started = false;

    this._responseData = null;
    this._statusCode = null;
    this._statusText = null;
    this._headers = null;

    this._onReadyStateChange = function () {
        if (_this._xmlHttpRequest.readyState === 4 ) {
            _this._clearTimer();
            _this._responseAvailable = true;

            _this._responseData = _this._xmlHttpRequest.responseText;
            _this._statusCode = _this._xmlHttpRequest.status;
            _this._statusText = _this._xmlHttpRequest.statusText;
            _this._headers = _this._xmlHttpRequest.getAllResponseHeaders();

            _this._webRequest.completed(Sys.EventArgs.Empty);
            if (_this._xmlHttpRequest != null) {
                _this._xmlHttpRequest.onreadystatechange = Function.emptyMethod;
                _this._xmlHttpRequest = null;
            }
        }
    }

    this._clearTimer = function this$_clearTimer() {
        if (_this._timer != null) {
            window.clearTimeout(_this._timer);
            _this._timer = null;
        }
    }

    this._onTimeout = function this$_onTimeout() {
        if (!_this._responseAvailable) {
            _this._clearTimer();
            _this._timedOut = true;
            _this._xmlHttpRequest.onreadystatechange = Function.emptyMethod;
            _this._xmlHttpRequest.abort();
            _this._webRequest.completed(Sys.EventArgs.Empty);
            _this._xmlHttpRequest = null;
        }
    }
}

This is very simple and clean, so I’ll just show you other methods of the XMLHttpSyncExecutor.

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
function Sys$Net$XMLHttpSyncExecutor$get_timedOut() {
    /// <value type="Boolean"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._timedOut;
}

function Sys$Net$XMLHttpSyncExecutor$get_started() {
    /// <value type="Boolean"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._started;
}

function Sys$Net$XMLHttpSyncExecutor$get_responseAvailable() {
    /// <value type="Boolean"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._responseAvailable;
}

function Sys$Net$XMLHttpSyncExecutor$get_aborted() {
    /// <value type="Boolean"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._aborted;
}

function Sys$Net$XMLHttpSyncExecutor$executeRequest() {
    if (arguments.length !== 0) throw Error.parameterCount();
    this._webRequest = this.get_webRequest();

    if (this._started) {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallOnceStarted, 'executeRequest'));
    }
    if (this._webRequest === null) {
        throw Error.invalidOperation(Sys.Res.nullWebRequest);
    }

    var body = this._webRequest.get_body();
    var headers = this._webRequest.get_headers();
    this._xmlHttpRequest = new XMLHttpRequest();
    this._xmlHttpRequest.onreadystatechange = this._onReadyStateChange;
    var verb = this._webRequest.get_httpVerb();
    this._xmlHttpRequest.open(verb, this._webRequest.getResolvedUrl(), false); // False to call Synchronously
    if (headers) {
        for (var header in headers) {
            var val = headers[header];
            if (typeof(val) !== "function")
                this._xmlHttpRequest.setRequestHeader(header, val);
        }
    }

    if (verb.toLowerCase() === "post") {
        if ((headers === null) || !headers['Content-Type']) {
            this._xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        }

        if (!body) {
            body = "";
        }
    }

    var timeout = this._webRequest.get_timeout();
    if (timeout > 0) {
        this._timer = window.setTimeout(Function.createDelegate(this, this._onTimeout), timeout);
    }
    this._xmlHttpRequest.send(body);
    this._started = true;
}

 function Sys$Net$XMLHttpSyncExecutor$getAllResponseHeaders() {
    /// <returns type="String"></returns>
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._responseAvailable) {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallBeforeResponse, 'getAllResponseHeaders'));
    }

    return this._headers;
}

function Sys$Net$XMLHttpSyncExecutor$get_responseData() {
    /// <value type="String"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._responseAvailable)
    {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallBeforeResponse, 'get_responseData'));
    }

    return this._responseData;
}

function Sys$Net$XMLHttpSyncExecutor$get_statusCode() {
    /// <value type="Number"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._responseAvailable)
    {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallBeforeResponse, 'get_statusCode'));
    }

    return this._statusCode;
}

function Sys$Net$XMLHttpSyncExecutor$get_statusText() {
    /// <value type="String"></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._responseAvailable)
    {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallBeforeResponse, 'get_statusText'));
    }

    return this._statusText;
}

function Sys$Net$XMLHttpSyncExecutor$get_xml() {
    /// <value></value>
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._responseAvailable)
    {
        throw Error.invalidOperation(String.format(Sys.Res.cannotCallBeforeResponse, 'get_xml'));
    }

    var xml = this._responseData;
    if ((!xml) || (!xml.documentElement))
    {
        xml = new XMLDOM(this._responseData);
        if ((!xml) || (!xml.documentElement))
        {
            return null;
        }
    }
    else if (navigator.userAgent.indexOf('MSIE') !== -1)
    {
        xml.setProperty('SelectionLanguage', 'XPath');
    }

    if ((xml.documentElement.namespaceURI === "http://www.mozilla.org/newlayout/xml/parsererror.xml") &&
        (xml.documentElement.tagName === "parsererror"))
    {
        return null;
    }

    if (xml.documentElement.firstChild && xml.documentElement.firstChild.tagName === "parsererror")
    {
        return null;
    }

    return xml;
}

function Sys$Net$XMLHttpSyncExecutor$abort() {
    if (arguments.length !== 0) throw Error.parameterCount();
    if (!this._started) {
        throw Error.invalidOperation(Sys.Res.cannotAbortBeforeStart);
    }

    if (this._aborted || this._responseAvailable || this._timedOut)
        return;

    this._aborted = true;

    this._clearTimer();

    if (this._xmlHttpRequest && !this._responseAvailable) {
        this._xmlHttpRequest.onreadystatechange = Function.emptyMethod;
        this._xmlHttpRequest.abort();

        this._xmlHttpRequest = null;
        var handler = this._webRequest._get_eventHandlerList().getHandler("completed");
        if (handler) {
            handler(this, Sys.EventArgs.Empty);
        }
    }
}

Sys.Net.XMLHttpSyncExecutor.prototype = {
    get_timedOut: Sys$Net$XMLHttpSyncExecutor$get_timedOut,
    get_started: Sys$Net$XMLHttpSyncExecutor$get_started,
    get_responseAvailable: Sys$Net$XMLHttpSyncExecutor$get_responseAvailable,
    get_aborted: Sys$Net$XMLHttpSyncExecutor$get_aborted,
    executeRequest: Sys$Net$XMLHttpSyncExecutor$executeRequest,
    getAllResponseHeaders: Sys$Net$XMLHttpSyncExecutor$getAllResponseHeaders,
    get_responseData: Sys$Net$XMLHttpSyncExecutor$get_responseData,
    get_statusCode: Sys$Net$XMLHttpSyncExecutor$get_statusCode,
    get_statusText: Sys$Net$XMLHttpSyncExecutor$get_statusText,
    get_xml: Sys$Net$XMLHttpSyncExecutor$get_xml,
    abort: Sys$Net$XMLHttpSyncExecutor$abort
}
Sys.Net.XMLHttpSyncExecutor.registerClass('Sys.Net.XMLHttpSyncExecutor', Sys.Net.WebRequestExecutor);

And finally we will notify ASP.NET AJAX that script have been loaded.

1
2
3
4
if (typeof(Sys) != 'undefined')
{
    Sys.Application.notifyScriptLoaded();
}

Usage is not so simple as wanted, but it’s still quite easy to understand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getServerTime() {
    // Instantiate a WebRequest.
    var wRequest = new Sys.Net.WebRequest();
    // Set the request URL.
    wRequest.set_url(PageMethods.get_path() + "/GetServerTime");
    // Set the request verb.
    wRequest.set_httpVerb('POST');
   
    wRequest.get_headers()['Content-Type'] = 'application/json; charset=utf-8';

    var executor = new Sys.Net.XMLHttpSyncExecutor();
    wRequest.set_executor(executor);
    // Execute the request.
    wRequest.invoke();

    if (executor.get_responseAvailable()) {
        return executor.get_object();
    }
    return false;
}

When you are using ScriptService, you will not see these difficulties, because ASP.NET will generate wrappers by itself. But if you need synchronous AJAX call — sorry, but in this case you should do it manually. Sample solution could be found here.

The post Synchronous page method call in ASP.NET AJAX library first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/synchronous-page-method-call-in-asp-net-ajax-library/feed/ 10
Culture-specific strings pluralization in .NET https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/ https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/#comments Wed, 29 Aug 2007 06:33:01 +0000 http://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/ Not so long ago I faced a problem of displaying count of some entities in singular or plural form (“No comments” or “less than a minute ago”, “1 comment” or “a minute ago”, “2 comments” or “2 minutes ago”). It’s very easy for English (there are three variants only), but I’m working on application that […]

The post Culture-specific strings pluralization in .NET first appeared on Dmytro Shteflyuk's Home.]]>
Not so long ago I faced a problem of displaying count of some entities in singular or plural form (“No comments” or “less than a minute ago”, “1 comment” or “a minute ago”, “2 comments” or “2 minutes ago”). It’s very easy for English (there are three variants only), but I’m working on application that should be localized for several cultures. For example, in Russian we have at least 4 forms (“Нет комментариев”, “1 комментарий”, “2 комментария”, “5 комментариев”) and not so obvious rules for plural forms (“11 комментариев”, “111 комментариев”, but “21 комментарий”). I don’t speak any other language, but I think that several of them might be even more complicated than Russian. Here you can find my thoughts about such strings localization.

ASP.NET (and as I know, Java, Ruby, etc) does not have built-in functionality for numeric strings localization. Yes, I know about Rubish pluralize, but it does not working for Russian, and I suspect for some other languages too. So I need to implement pluralization, desirable using ASP.NET resources, with simplest interface as possible. Adding new languages should be easy enough.

So, here is my idea. We will define several strings in our resources (Comments0, Comments1, Comments2, etc). We have some interface, for example IResourceIndexer. Interface has single method, which should return resource index by entities number. We have to implement this interface for different languages (English, Russian, etc). Then we need to create factory, which would return culture-specific IResourceIndexer. And latest step is to create static class with helper methods, which will return strings using number of entities. But seeing is believing. Let’s look at the code.

Here is interface. As you can see, it’s very simple — just single method, which returns resource index.

1
2
3
4
5
6
7
namespace App_Code
{
    public interface IResourceIndexer
    {
        int GetResourceIndex(long count);
    }
}

[lang_en]
Let’s implement indexers for English and Russian languages:
[/lang_en]
[lang_ru]
Давайте реализуем индексаторы для английского и русского языков:
[/lang_ru]

1
2
3
4
5
6
7
8
9
10
11
12
namespace App_Code
{
    public class EnglishResourceIndexer : IResourceIndexer
    {
        public int GetResourceIndex(long count)
        {
            if (count == 0) return 0;
            if (count == 1) return 1;
            return 2;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace App_Code
{
    public class RussianResourceIndexer : IResourceIndexer
    {
        public int GetResourceIndex(long count)
        {
            if (count == 0) return 0;
            if (count == 1) return 1;

            int twoDigits = (int) count % 100;
            if (twoDigits > 10 && twoDigits < 20) return 3;

            int lastDigit = (int) (count % 10);
            if (lastDigit == 1) return 4;
            if (lastDigit > 1 && lastDigit < 5) return 2;
            return 3;
        }
    }
}

As you can see, Russian is more difficult than English :-) But it still easy enough to implement. Hope, for other languages we could do something like this without any problems. Please note, that I have added different resource indexes for 1 and 21 in Russian. Usually this is not needed, but I want to have ability to use string “минуту назад” for 1 and “21 минуту назад” for 21.

Now we need to define factory to get indexer using current culture (which could be accessed through Thread.CurrentThread.CurrentUICulture property).

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
using System.Collections.Generic;
using System.Globalization;
using System.Threading;

namespace App_Code
{
    public static class NumericResourceFactory
    {
        static NumericResourceFactory()
        {
            _resourceIndexerCache = new Dictionary<string, IResourceIndexer>();
        }

        public static IResourceIndexer GetResourceIndexer()
        {
            CultureInfo culture = Thread.CurrentThread.CurrentUICulture;
            return GetResourceIndexer(culture);
        }

        private static IResourceIndexer GetResourceIndexer(CultureInfo culture)
        {
            string id = culture.TwoLetterISOLanguageName;
            if (!_resourceIndexerCache.ContainsKey(id))
            {
                switch(id)
                {
                    case "ru":
                        _resourceIndexerCache[id] = new RussianResourceIndexer();
                        break;
                    default:
                        _resourceIndexerCache[id] = new EnglishResourceIndexer();
                        break;
                }
            }
            return _resourceIndexerCache[id];
        }

        private static Dictionary<string, IResourceIndexer> _resourceIndexerCache;
    }
}

As you can see, I’m detecting indexer using two letters ISO code. Of course, you can do more complex processing here. Please note, that English is default language in my application, you might need to change cases order. I’m using indexers cache in this code to avoid factory performance hit.

Almost finished. Now we need to define resources and implement helper class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace App_Code
{
    public static class ResourceStrings
    {
        public static string GetCommentsString(int comments)
        {
            IResourceIndexer indexer = NumericResourceFactory.GetResourceIndexer();
            string format = GetResourceString("Comments" + indexer.GetResourceIndex(comments));
            return String.Format(format, comments);
        }

        private static string GetResourceString(string id)
        {
            return Resources.NumericResources.ResourceManager.GetString(id,
                Resources.NumericResources.Culture);
        }
    }
}

And resources:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Comments0" xml:space="preserve">
    <value>No comments</value>
    <comment>0 comments</comment>
  </data>
  <data name="Comments1" xml:space="preserve">
    <value>1 comment</value>
    <comment>1 comment</comment>
  </data>
  <data name="Comments2" xml:space="preserve">
    <value>{0} comments</value>
    <comment>2 comments (and more)</comment>
  </data>
  <data name="Comments3" xml:space="preserve">
    <value />
    <comment>not used in English</comment>
  </data>
  <data name="Comments4" xml:space="preserve">
    <value />
    <comment>not used in English</comment>
  </data>
</root>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Comments0" xml:space="preserve">
    <value>Нет комментариев</value>
    <comment>0 комментариев</comment>
  </data>
  <data name="Comments1" xml:space="preserve">
    <value>{0} комментарий</value>
    <comment>1 комментарий</comment>
  </data>
  <data name="Comments2" xml:space="preserve">
    <value>{0} комментария</value>
    <comment>2-4 комментария</comment>
  </data>
  <data name="Comments3" xml:space="preserve">
    <value>{0} комментариев</value>
    <comment>5-9 комментариев</comment>
  </data>
  <data name="Comments4" xml:space="preserve">
    <value>{0} комментарий</value>
    <comment>21 комментарий</comment>
  </data>
</root>

Please note, that I have 5 resource strings in English strings file, in spite of only 3 really need. That’s because English is my default language, and to build my satellite assembly with Russian resources, I must have same string resources in both resource files.

Little more about usage. First you should initialize thread culture. In ASP.NET applications you should override method InitializeCulture and set Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture properties:

1
2
3
4
5
6
7
8
9
10
11
12
protected override void InitializeCulture()
{
    CultureInfo culture;
    if (Request.UserLanguages != null && Request.UserLanguages.Length > 0)
        culture = CultureInfo.CreateSpecificCulture(Request.UserLanguages[0]);
    else
        culture = CultureInfo.CreateSpecificCulture("");

    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
    base.InitializeCulture();
}

In this case browser culture would be used (in Firefox — Tools/Options/Advanced/Languages -> Choose, add Russian or English as first language; in Internet Explorer it could be done here — Tools/Internet Options/Languages.) Now you can write something like this in .aspx file:

1
2
3
4
<%@ Import namespace="App_Code" %>
<asp:Label runat="server">
    <%= ResourceStrings.GetCommentsString(20) %>
</asp:Label>

Full example project could be downloaded here. Have any comments?

The post Culture-specific strings pluralization in .NET first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/feed/ 5
GridView with custom Digg-like pagination https://kpumuk.info/asp-net/gridview-with-custom-digg-like-pagination/ https://kpumuk.info/asp-net/gridview-with-custom-digg-like-pagination/#comments Mon, 27 Aug 2007 05:42:03 +0000 http://kpumuk.info/asp-net/gridview-with-custom-digg-like-pager/ GridView is a great highly customizable ASP.NET control. Today I want to show, how to create derived control, which allows to add Digg-style pagination to your application. First, we will create derived control and add property UseCustomPager, which will define whether or not to use Digg-style pagination: 123456789101112131415161718using System; using System.Globalization; using System.Reflection; using System.Web.UI; […]

The post GridView with custom Digg-like pagination first appeared on Dmytro Shteflyuk's Home.]]>
GridView is a great highly customizable ASP.NET control. Today I want to show, how to create derived control, which allows to add Digg-style pagination to your application.

First, we will create derived control and add property UseCustomPager, which will define whether or not to use Digg-style pagination:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Globalization;
using System.Reflection;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace App_Code
{
    public class GridViewWithPager : GridView
    {
        public bool UseCustomPager
        {
            get { return (bool?) ViewState["UseCustomPager"] ?? false; }
            set { ViewState["UseCustomPager"] = value; }
        }
    }
}

GridView has virtual method InitializePager which could be overridden to create our custom pager:

1
2
3
4
5
6
7
protected override void InitializePager(GridViewRow row, int columnSpan, PagedDataSource pagedDataSource)
{
    if (UseCustomPager)
        CreateCustomPager(row, columnSpan, pagedDataSource);
    else
        base.InitializePager(row, columnSpan, pagedDataSource);
}

Now let’s create our custom pager:

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
protected virtual void CreateCustomPager(GridViewRow row, int columnSpan, PagedDataSource pagedDataSource)
{
    int pageCount = pagedDataSource.PageCount;
    int pageIndex = pagedDataSource.CurrentPageIndex + 1;
    int pageButtonCount = PagerSettings.PageButtonCount;

    TableCell cell = new TableCell();
    row.Cells.Add(cell);
    if (columnSpan > 1) cell.ColumnSpan = columnSpan;

    if (pageCount > 1)
    {
        HtmlGenericControl pager = new HtmlGenericControl("div");
        pager.Attributes["class"] = "pagination";
        cell.Controls.Add(pager);

        int min = pageIndex - pageButtonCount;
        int max = pageIndex + pageButtonCount;

        if (max > pageCount)
            min -= max - pageCount;
        else if (min < 1)
            max += 1 - min;

        // Create "previous" button
        Control page = pageIndex > 1
                        ? BuildLinkButton(pageIndex - 2, PagerSettings.PreviousPageText, "Page", "Prev")
                        : BuildSpan(PagerSettings.PreviousPageText, "disabled");
        pager.Controls.Add(page);

        // Create page buttons
        bool needDiv = false;
        for (int i = 1; i <= pageCount; i++)
        {
            if (i <= 2 || i > pageCount - 2 || (min <= i && i <= max))
            {
                string text = i.ToString(NumberFormatInfo.InvariantInfo);
                page = i == pageIndex
                        ? BuildSpan(text, "current")
                        : BuildLinkButton(i - 1, text, "Page", text);
                pager.Controls.Add(page);
                needDiv = true;
            }
            else if (needDiv)
            {
                page = BuildSpan("&hellip;", null);
                pager.Controls.Add(page);
                needDiv = false;
            }
        }

        // Create "next" button
        page = pageIndex < pageCount
                ? BuildLinkButton(pageIndex, PagerSettings.NextPageText, "Page", "Next")
                : BuildSpan(PagerSettings.NextPageText, "disabled");
        pager.Controls.Add(page);
    }
}

private Control BuildLinkButton(int pageIndex, string text, string commandName, string commandArgument)
{
    PagerLinkButton link = new PagerLinkButton(this);
    link.Text = text;
    link.EnableCallback(ParentBuildCallbackArgument(pageIndex));
    link.CommandName = commandName;
    link.CommandArgument = commandArgument;
    return link;
}

private Control BuildSpan(string text, string cssClass)
{
    HtmlGenericControl span = new HtmlGenericControl("span");
    if (!String.IsNullOrEmpty(cssClass)) span.Attributes["class"] = cssClass;
    span.InnerHtml = text;
    return span;
}

Several pager settings used in this code:

  • PagerSettings.PreviousPageText — text to be shown on the “Previous” button.
  • PagerSettings.NextPageText — text to be shown on the “Next” button.
  • PagerSettings.PageButtonCount — how many pages to show before and after the current page.

You could see, that in BuildLinkButton method I have used custom control PagerLinkButton. This is just descendant of the LinkButton control which simplifies usage inside our GridViewWithPager:

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
using System;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace App_Code
{
    public class PagerLinkButton : LinkButton
    {
        public PagerLinkButton(IPostBackContainer container)
        {
            _container = container;
        }

        public void EnableCallback(string argument)
        {
            _enableCallback = true;
            _callbackArgument = argument;
        }

        public override bool CausesValidation
        {
            get { return false; }
            set { throw new ApplicationException("Cannot set validation on pager buttons"); }
        }

        protected override void Render(HtmlTextWriter writer)
        {
            SetCallbackProperties();
            base.Render(writer);
        }

        private void SetCallbackProperties()
        {
            if (_enableCallback)
            {
                ICallbackContainer container = _container as ICallbackContainer;
                if (container != null)
                {
                    string callbackScript = container.GetCallbackScript(this, _callbackArgument);
                    if (!string.IsNullOrEmpty(callbackScript)) OnClientClick = callbackScript;
                }
            }
        }

        #region Private fields

        private readonly IPostBackContainer _container;
        private bool _enableCallback;
        private string _callbackArgument;

        #endregion
    }
}

Our control is almost done. All we need is to define method ParentBuildCallbackArgument. As you could see from GridView sources, this method is used for serializing page index, sort direction and sort expression, but for some reason, it has been defined as internal. I don’t like hacks, but in this case I think that I can cheat a little:

1
2
3
4
5
6
7
private string ParentBuildCallbackArgument(int pageIndex)
{
    MethodInfo m =
        typeof (GridView).GetMethod("BuildCallbackArgument", BindingFlags.NonPublic | BindingFlags.Instance, null,
                                    new Type[] {typeof (int)}, null);
    return (string) m.Invoke(this, new object[] {pageIndex});
}

BTW, as you could see, I have not added any comments to methods. It is done specially, because I don’t want to create custom controls library, just sharing my experience :-)

And now I’ll show you example of usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<asp:XmlDataSource runat="server" ID="xdsCountries"
    DataFile="~/App_Data/CountryCodeList.xml" />

<ac:GridViewWithPager runat="server" UseCustomPager="true" AllowPaging="true"
    DataSourceID="xdsCountries" PageSize="10" AutoGenerateColumns="false">
    <PagerSettings PreviousPageText="&laquo; previous"
        NextPageText="next &raquo;" PageButtonCount="3" />
    <Columns>
        <asp:TemplateField HeaderText="Code">
            <ItemTemplate><%# XPath("CountryCoded") %></ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Name">
            <ItemTemplate><%# XPath("CountryName") %></ItemTemplate>
        </asp:TemplateField>
    </Columns>
</ac:GridViewWithPager>

And a screenshot:

Digg-style pagination

Full source code could be downloaded here.

The post GridView with custom Digg-like pagination first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/gridview-with-custom-digg-like-pagination/feed/ 13
Using Panel.DefaultButton property with LinkButton control in ASP.NET https://kpumuk.info/asp-net/using-panel-defaultbutton-property-with-linkbutton-control-in-asp-net/ https://kpumuk.info/asp-net/using-panel-defaultbutton-property-with-linkbutton-control-in-asp-net/#comments Sat, 25 Aug 2007 10:53:42 +0000 http://kpumuk.info/asp-net/using-panel-defaultbutton-property-with-linkbutton-control-in-asp-net/ ASP.NET has great limitation — only one server form on the page. Even if you are developing complex page, which looks like several forms with different submit buttons, in fact you have only one form. So we have a problem: How browser understanding which button should be triggered when user pressed ENTER? It uses first […]

The post Using Panel.DefaultButton property with LinkButton control in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
ASP.NET has great limitation — only one server form on the page. Even if you are developing complex page, which looks like several forms with different submit buttons, in fact you have only one form. So we have a problem: How browser understanding which button should be triggered when user pressed ENTER? It uses first Button control (usually that’s wrong), and if you have LinkButton‘s, they would never be triggered. In ASP.NET 2.0 new property has been added to the Panel and HtmlForm controls — DefaultButton, which can be used to specify ID of the control, which implements IButtonControl interface (usually Button and LinkButton). This button control would be triggered when user pressed ENTER. But there is one big problem exist: LinkButton control would not be triggered in Firefox on ENTER. In this article I will show why this problem take place and how to solve it.

Let’s look how DefaultButton is working in ASP.NET. Here is simple example:

1
2
3
4
5
<asp:Label runat="server" ID="lblHello" />
<asp:Panel runat="server">
    First name: <asp:TextBox runat="server" ID="txtFirstName" />
    <asp:LinkButton ID="lbHello" runat="server" Text="Click me" OnClick="lbHello_Click" />
</asp:Panel>
1
2
3
4
protected void lbHello_Click(object sender, EventArgs e)
{
    lblHello.Text = String.Format("Hello, {0}", txtFirstName.Text);
}

When you will try to press ENTER button inside the text box, form will be submitted to the server, but LinkButton‘s Click event will not be fired. Let’s try to specify DefaultButton property of the panel:

1
2
3
4
<asp:Panel runat="server" DefaultButton="lbHello">
    First name: <asp:TextBox runat="server" ID="txtFirstName" />
    <asp:LinkButton ID="lbHello" runat="server" Text="Click me" OnClick="lbHello_Click" />
</asp:Panel>

Great! It’s working perfectly in Internet Explorer, but what about Firefox? Oops, we found a problem. You could find solution at the end of the post, but first I want to show why this problem occurs. When you specify DefaultButton property, ASP.NET generates following code:

1
2
<div onkeypress="javascript:return WebForm_FireDefaultButton(event, 'lbHello')">
</div>

Here is WebForm_FireDefaultButton method, defined in ASP.NET JavaScript library:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function WebForm_FireDefaultButton(event, target) {
    if (event.keyCode == 13 && !(event.srcElement && (event.srcElement.tagName.toLowerCase() == "textarea"))) {
        var defaultButton;
        if (__nonMSDOMBrowser) {
            defaultButton = document.getElementById(target);
        }
        else {
            defaultButton = document.all[target];
        }
        if (defaultButton && typeof(defaultButton.click) != "undefined") {
            defaultButton.click();
            event.cancelBubble = true;
            if (event.stopPropagation) event.stopPropagation();
            return false;
        }
    }
    return true;
}

The problem occurs in the code typeof(defaultButton.click) != "undefined" — Firefox does not define click() method for the a element, so the form will be submitted without __EVENTTARGET parameter, and button will not be triggered. The simplest way to solve the problem is to define click() method for the a:

1
2
3
4
5
6
7
8
9
10
var b = document.getElementById('<%= lbHello.ClientID %>');
if (b && typeof(b.click) == 'undefined') {
    b.click = function() { 
        var result = true;
        if (b.onclick) result = b.onclick();
        if (typeof(result) == 'undefined' || result) {
            eval(b.getAttribute('href'));
        }
    }
}

Our click() method imitates a element behavior: first it calls onclick() method (OnClientClick attribute of the LinkButton), and if it returns false — stops processing. Then it evals href attribute (where ASP.NET puts __doPostBack call).

Let’s create custom control which encapsulates this logic:

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
using System;
using System.Web.UI.WebControls;

namespace App_Code
{
    public class LinkButtonDefault : LinkButton
    {
        protected override void OnLoad(System.EventArgs e)
        {
            Page.ClientScript.RegisterStartupScript(GetType(), "addClickFunctionScript",
                _addClickFunctionScript, true);

            string script = String.Format(_addClickScript, ClientID);
            Page.ClientScript.RegisterStartupScript(GetType(), "click_" + ClientID,
                script, true);
            base.OnLoad(e);
        }

        private const string _addClickScript = "addClickFunction('{0}');";

        private const string _addClickFunctionScript =
            @"  function addClickFunction(id) {{
            var b = document.getElementById(id);
            if (b && typeof(b.click) == 'undefined') b.click = function() {{
                var result = true; if (b.onclick) result = b.onclick();
                if (typeof(result) == 'undefined' || result) {{ eval(b.getAttribute('href')); }}
            }}}};"
;
    }
}

And corresponding .aspx file:

1
2
3
4
5
6
<%@ Register Namespace="App_Code" TagPrefix="ac" %>
<asp:Label runat="server" ID="lblHello" />
<asp:Panel runat="server" DefaultButton="lbHello">
    First name: <asp:TextBox runat="server" ID="txtFirstName" />
    <ac:LinkButtonDefault ID="lbHello" runat="server" Text="Click me" OnClick="lbHello_Click" />
</asp:Panel>

Of course, JavaScript code should be moved out from the C# code, but for my example it’s good enough. If you are using ASP.NET AJAX library, you should use ScriptManager.RegisterStartupScript method instead of ClientScriptManager.RegisterStartupScript.

Hope, this post will help you. Your comments are welcome.

The post Using Panel.DefaultButton property with LinkButton control in ASP.NET first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/using-panel-defaultbutton-property-with-linkbutton-control-in-asp-net/feed/ 23