Sunday, June 26, 2016

System design with ASHX web services (JQuery + AJAX + partial HTML UI)

Recently, we are working on a few new systems with ASP.NET. In the past, we are using Page (ASPX) + ScriptManager and we are facing some limitation in the system design which includes the following:
  • The system does not allow the user to add a new "item" into the drop down list on the fly.
  • The drop down list contains a few hundred items and we need to incorporate the 'search' functionality or paging to avoid all items to be loaded in one shot.
  • The data submitted to server failed to meet the validation process and causing the entire page sent back to the client (which travel from client to the server and then back to the client).
To solve these types of problem, we need to have a new design to the foundation of the system.
  • We must use JQuery + AJAX + partial HTML UI design so that it allows the user from adding "item" to the drop down list on the fly. The partial HTML UI will appear on the screen as a popup for user adding new item. After the user has submitted the new item to the server, validated OK and it will be added to the drop down list on the fly (with JQuery) without reloading the page or navigating to another page.
  • The drop down list that contains lots of item will be replace by a textbox. Upon the user clicking on this textbox, the items will be loaded from the server (with AJAX calls) and then display in the popup (with JQuery + partial HTML UI). To improve the user's experience, you may consider the auto-complete feature upon the user typing the text or divide the data using the paging concept.
  • We must do more AJAX calls for submitting the user input to be validated by the system. In case of any failure such as validation failed, the server should returns the error message only. This avoids the entire page to be re-created in the server and then send to the browser.
With the new design, the system becomes more responsive and lesser network traffic. But, we still have a problem on how to handle the AJAX call. Are we going to have one ASHX to handle one process (that will end up on lots of ASHX)? Or are we going to have only one ASHX entry point that handles all the requests?

To solve this problem, here is the list of frequent use "web service" to be implemented with Handler (ASHX):
  • ~/q  - this web service handles the "query" that includes CRUD (create, return, update & delete) processes, daily process, ad-hoc process and all other business processes. The report request is another area which you may consider to put into this service.
  • ~/t - this web service returns the "HTML template" (partial HTML UI design) to be injected to the current web page. By creating the partial HTML UI file, it allows the designer to work on the layout without have to go through all the JQuery + the DOM element generation (i.e., low level stuffs). Modifying the DOM elements using JQquery is very time consuming and it requires a more expensive Javascript programmer. But, we have done it with a cheaper costing. The nice partial HTML UI has been done by the designer and the programmer requires to populate the JSON data into the appropriate placeholder.
  • ~/f - this web service handles all the file services that include upload, download/view. For example, when the user calls out the "contact list", it shows the profile photo of the contact. This profile photo IMG SRC is "~/f?contact_id=124567" where "contact_id" is the primary key value of the contact. It does not point to any physical file name. The "f" service will do all the necessary at the server side and returns the binary of the photo (an image file).
To setup the above shortcut, you have to create mapped URL in web.config. For example,

  <system.web>
    <urlMappings>
      <add url="~/q" mappedUrl="~/myWebService/q.ashx"/>
    </urlMappings>
  </system.web>

The design these web services:
  • Client is making a request to the web service:
    • "code" - the command code, object type or process code to be executed.
    • "action" - this includes CRUD and other actions (such as run daily job, run hour job).
    • "query parameters" - the query parameters are wrapped into a JSON object. For example, the client is requesting the customers who is owing more than $10,000 for more than 90 days.
  • Responding to the client:
    • "msg" - the message to the client. "ok" to indicate the query has successfully executed. Otherwise, it contains the error message.
    • "list" - the list of JSON formatted data requested by the client. This information is optional.
Some of you might be thinking why we are not using REST for these web services. The answer is simple: we don't need to given definition to the "method". Such as PUT or POST method. What if the user wants to execute an ad-hoc process (POST or custom method)?



Thursday, January 14, 2016

WebSocket

Implementing WebSocket in ASP.Net is quite easy. You need 2 components:

1. The ASHX that handle the web socket communcation.
2. The client side Javascript which sends message to the server and waiting for server message.

The WSHandler.ashx page resides in "WSChat" folder

<%@ WebHandler Language="C#" Class="WSHandler" %>
using System;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using System.Web.WebSockets;
using System.Net.WebSockets;
using System.Text;
using System.Collections.Generic;
using System.Linq;

//22.Dec.15,lhw-
public class WSHandler : IHttpHandler
{

    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(ProcessWSChat);
        }
    }

    public bool IsReusable { get { return false; } }


    private async Task ProcessWSChat(AspNetWebSocketContext context)
    {
        WebSocket socket = context.WebSocket;

        //<<=======
        MyConnection cn = new MyConnection(socket);
        _conn_list.Add(cn);
        //<<=======

        while (true)
        {
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);

            WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);

            //------------------------------------------------------------------------------
            if (socket.State == WebSocketState.Open)
            {
                string userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);


                if (userMessage.StartsWith("helo from"))
                {
                    // user has signed in with his ID.
                    cn.uid = userMessage.Substring("helo from".Length, userMessage.Length - "helo from".Length).TrimStart();
                }
                else if (userMessage.ToLower().StartsWith("b:"))
                {
                    // broadcast the message.
                    userMessage = "You sent: " + userMessage + " at " + DateTime.Now.ToLongTimeString();
                    await Broadcast(userMessage);
                }
                else
                {
                    // echo the message
                    userMessage = "You sent: " + userMessage + " at " + DateTime.Now.ToLongTimeString();
                    buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(userMessage));
                    await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            //------------------------------------------------------------------------------
            else if (socket.State == WebSocketState.CloseReceived)
            {
                // remove current connection from the memory
                var v = _conn_list.Where(n => n.sess_id == cn.sess_id).FirstOrDefault();
                if (v != null)
                {
                    _conn_list.Remove(v);
                }

                // inform everyone that the current user has left the chat.
                await Broadcast(cn.uid + " has signed out");
            }
            else
            {
                break;
            }
        }
    }

    //------------------------------------------------------------------------------
    async Task Broadcast(string msg)
    {
        ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg));

        foreach (var item in _conn_list)
        {
            await item.socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
        }
    }

    //------------------------------------------------------------------------------
    public class MyConnection
    {

        public string sess_id { get; private set; }
        public DateTime connected_on { get; private set; }
        public WebSocket socket { get; set; }

        // value from the browser. The user must send 'helo from xxx' where 'xxx' is the user id.
        public string uid { get; set; }

        public List<string> chat_room_list { get; set; }

        public MyConnection(WebSocket sk)
        {
            this.sess_id = Guid.NewGuid().ToString();
            this.connected_on = DateTime.Now;
            this.chat_room_list = new List<string>();
            this.socket = sk;
        }
    }

    static List<MyConnection> _conn_list = new List<MyConnection>();

}

The client side JavaScript

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>WebSocket Chat</title>
    <script type="text/javascript" src="js/jquery-1.7.js"></script>
    <script type="text/javascript">
        var ws;
        $().ready(function () {
            $("#btnConnect").click(function () {
                $("#spanStatus").text("connecting");
                ws = new WebSocket("ws://" + window.location.hostname +
                    ":64495/WSChat/WSHandler.ashx");
                ws.onopen = function () {
                    $("#spanStatus").text("connected");
                    post_msg();
                };
                ws.onmessage = function (evt) {
                    $("#spanStatus").text(evt.data);
                };
                ws.onerror = function (evt) {
                    $("#spanStatus").text(evt.message);
                };
                ws.onclose = function () {
                    $("#spanStatus").text("disconnected");
                };
            });
            $("#btnSend").click(function () {
                if (ws.readyState == WebSocket.OPEN) {
                    ws.send($("#textInput").val());
                }
                else {
                    $("#spanStatus").text("Connection is closed");
                }
            });

            $("#btnDisconnect").click(function () {
                ws.close();
            });

            var _timer = null;
            function post_msg() {
                _timer = window.setInterval(function () {
                    send_msg('helo from lau - ' + (new Date()).getSeconds().toString());
                }, 300);
            }
            function send_msg(s) {
                if (_timer != null) {
                    clearInterval(_timer);
                }

                if (ws.readyState == WebSocket.OPEN) {
                    ws.send(s);
                }
                else {
                    $("#spanStatus").text("Connection is closed");
                }
            }
        });
    </script>
</head>
<body>
    <input type="button" value="Connect" id="btnConnect" />
    <input type="button" value="Disconnect" id="btnDisconnect" /><br />
    <input type="text" id="textInput" />
    <input type="button" value="Send" id="btnSend" /><br />
    <span id="spanStatus">(display)</span>
</body>
</html>

Friday, September 18, 2015

Intelli-sense for Javascript in Visual Studio

After you have enabled the Intelli-sense for Javascript in Visual Studio, the JQuery methods were still not available in your ".js" file, this is because you might have missed out the following directive:


    /// <reference path="jquery-1.7.js" />


Saturday, August 1, 2015

Failed to deserialize the object due to DLL version/name has changed

When you tried to deserialize the binary to an object but you encountered the following exception:

  BinaryFormatter.Deserialize “unable to find assembly”

Basically, it tells you that it cannot find the DLL by version + name. This is commonly issue when you change the DLL version number or move the class to another project/assembly. As a result, we need a way to tell the BinaryFormatter class what is the correct new "type" for the binary.

In the deserialization process, you need to add a line to use your custom binder:

using (MemoryStream memory = new MemoryStream(user_input))
{
    BinaryFormatter binary = new BinaryFormatter();

    //fix the deserialization error when the DLL version has been changed.
    binary.Binder = new PreMergeToMergedDeserializationBinder();

    // convert the binary to the list.
    this._data = binary.Deserialize(memory) as List<CUserDataItem>;
}

And then add the following class. I have enhanced this class and it is able to handle the generic list as well.

public sealed class PreMergeToMergedDeserializationBinder : System.Runtime.Serialization.SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        Type typeToDeserialize = null;

        // For each assemblyName/typeName that you want to deserialize to
        // a different type, set typeToDeserialize to the desired type.
        String exeAssembly = Assembly.GetExecutingAssembly().FullName;
      
        // The following line of code returns the type.

        // extract the 'old dll name/version'.
        string old_dll = typeName.ExtractString(',', ']');

        if (old_dll.IsNotEmpty())
        {
            // for generic list, we replace the dll name/version here.
            typeToDeserialize = Type.GetType(typeName.Replace(old_dll, exeAssembly.ToString()));
        }
        else
        {
            // for 1 single object, the 'typeName' is the class name.
            // We should return the type name with the new dll name/version.
            typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
                                                typeName, exeAssembly));
        }

        System.Diagnostics.Debug.Assert(typeToDeserialize != null);

        return typeToDeserialize;
    }
}

I have an string class extension which helps to extract partial string:

public static string ExtractString(this string s,
    char start_char,
    char end_char)
{
    int i = s.IndexOf(start_char);
    if (i >= 0)
    {
        //16.Nov.2011-lhw-the 'end_char' should be search after the 'start_char'.
        int i2 = s.IndexOf(end_char,
                           i + 1);      //16.Nov.2011-lhw-missing the start pos!!

        string tmp = s.Substring(i + 1,
                                 i2 - i - 1);

        return tmp;
    }
    else
    {
        return string.Empty;
    }
}

Reference:
http://stackoverflow.com/questions/5170333/binaryformatter-deserialize-unable-to-find-assembly-after-ilmerge

Thursday, July 2, 2015

Routing to ASHX


Here is the piece of code that I found in CodeProject.com. By adding this extention method, you will be able to route the request to ASHX:

namespace System.Web.Routing
{
    public class HttpHandlerRoute : IRouteHandler
    {
        private String _virtualPath = null;
        private IHttpHandler _handler = null;

        public HttpHandlerRoute(String virtualPath)
        {
            _virtualPath = virtualPath;
        }

        public HttpHandlerRoute(IHttpHandler handler)
        {
            _handler = handler;
        }

        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            IHttpHandler result;
            if (_handler == null)
            {
                result = (IHttpHandler)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(IHttpHandler));
            }
            else
            {
                result = _handler;
            }
            return result;
        }
    }

    public static class RoutingExtensions
    {
        public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, string physicalFile, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
        {
            var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(physicalFile));
            RouteTable.Routes.Add(routeName, route);
        }

        public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, IHttpHandler handler, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
        {
            var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(handler));
            RouteTable.Routes.Add(routeName, route);
        }
    }
}

To access the routing data in ASHX, you need to do this:


            var o = context.Request.RequestContext.RouteData.Values["id"];
            if (o != null)
            {
                q = o.ToString();
            }

Reference:
http://www.codeproject.com/Tips/272258/ASP-net-HttpHandler-Routing-Support

Posting data in JSON format to the ASP.NET website using WinForm

Previously, we have shown how to post JSON data using JQuery to ASP.NET.

  http://laucsharp.blogspot.com/2013/03/posting-data-in-json-format-to-aspnet.html

Now, we are going to post JSON data using WinForm:

This is our business object which will reside at the server and client.

    public class Class1
    {
        public string code { get; set; }
        public string name { get; set; }

        public override string ToString()
        {
            return string.Format("code={0}, name={1}",
                this.code,
                this.name);
        }
    }

In the WinForm client program, when the user hit Button1 after keyed in the client code and name, the data will be submitted to the server:

        private void button1_Click(object sender, EventArgs e)
        {
            // store the user input into the business object.
            Class1 data = new Class1();
            data.code = this.client_code.Text;
            data.name = this.client_name.Text;

            // convert it into json format.
            JavaScriptSerializer js = new JavaScriptSerializer();
            string json_data = js.Serialize(data);

            // create the web request.
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create("http://localhost:57655/dataGateway.ashx");
            request.ContentType = "application/json;";           
            request.Method = "POST";

            // write the json data into the request stream.
            using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
            {
                writer.Write(json_data);
            }

            // get the server response.
            using (WebResponse response = request.GetResponse())
            {
                // read the server response.
                Stream response_stream = response.GetResponseStream();
                using (StreamReader r = new StreamReader(response_stream))
                {
                    // do what ever you want with the response.
                    this.label5.Text = r.ReadToEnd();
                }
            }           
        }

Finally, at the server side, we add a Generic Handler (dataGateway.ashx) and it looks like this:

<%@ WebHandler Language="C#" Class="dataGateway" %>

using System;
using System.Web;
using System.IO;
using System.Web.Script.Serialization;

public class dataGateway : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "text/plain";

        string s;
       
        // get the contents from the request stream
        Stream stream = context.Request.InputStream;
        using (StreamReader r = new StreamReader(stream))
        {
            s = r.ReadToEnd();
        }

        // ensure that the content is not empty.
        if (string.IsNullOrEmpty(s) || s.Length == 0)
        {
            context.Response.Write("'data' cannot be blank");
            return;
        }

        // convert it from json format to our business object
        JavaScriptSerializer js = new JavaScriptSerializer();
        Class1 obj = js.Deserialize<Class1>(s);

        // do whatever you want
        context.Cache["data"] = obj;

        // returns the response code/status to the caller.
        context.Response.Write("ok. received the data =>" + s);
    }

    public bool IsReusable { get { return false; } }
}

Next, sending compressed data in WinForm:

    http://laucsharp.blogspot.my/2018/04/posting-compressed-data-in-json-format.html

Thursday, June 18, 2015

Excluding folders upon publishing the website

In VS2013, after you have setup the "publish" (right click on the website and choose Publish Web Site), a new configuration file (website.publishproj) will be added to the project.

To exclude the folders, you have to open this file and add the following section with the "project" section:

  <ItemGroup>
    <ExcludeFromPackageFolders Include="log;temp;">
      <FromTarget>Remove temp folders</FromTarget>
    </ExcludeFromPackageFolders>
  </ItemGroup>

In the "Include" attribute, it contains the folder to be removed when publishing the website. The above example excluding "log" and "temp" folders.