Andri Yadi

A geeky technopreneur, trying to do something big with his startup

June 2008 - Posts

  • GCOE - Web Site Publik Berorientasi Kependudukan

    Pada hari kamis, 26 Juni 2008 lalu, saya berkesempatan mengisi batch terakhir dari serangkaian kegiatan Government Center of Excellence (GCOE) training di Microsoft Innovation Center (MIC) ITB. Training terakhir ini bertajuk "Web Site Publik Berorientasi Kependudukan". Lebih jauh ttg GCOE training bisa dilihat di sini dan di sini.

    Pada hari yang sama, di MIC UI juga berlangsung training GCOE dengan judul yang sama, yang di-deliver oleh Agung Riyadi. Jika Agung membahas Windows Live dan DotNetNuke-nya, seperti yang ia tulis di sini, saya membahas ASP.NET Dynamic Data sebagai teknologi utama untuk membangun web site berorientasi kependudukan. Honestly, tidak terlalu jelas juga apa yang dimaksud "Website Publik Berorientasi Kependudukan". Apakah yang dimaksud itu adalah aplikasi untuk mengelola data penduduk, atau website yang menawarkan services (apapun) bagi citizen, atau yang lainnya. Tapi satu yang pasti, saya berhasil memukau peserta - yang semuanya berasal dari Pemerintah Daerah - dengan membuat aplikasi manajemen data penduduk dalam waktu kurang dari 2 menit. Saya bahkan meminta salah satu peserta untuk mengaktifkan timer dan mulai menghitung bertepatan dengan saya membuka Visual Studio. Hasilnya, kurang dari 2 menit, sebuah aplikasi yang fully functional-pun selesai. That's the magic that ASP.NET Dynamic Data can bring :)

    Mungkin ada yang berpendapat, kok tega2nya saya "membodohi" peserta dengan teknologi "fast food". Well, ada beberapa alasan:

    1. Walaupun punya background programming, tidak satupun dari peserta yang hadir pernah menyentuh .NET, ASP.NET, or any .NET technologies. So, daripada membuat mereka pusing dengan mengajarkan .NET, ASP.NET, AJAX, LINQ, LINQ to SQL dari awal, why don't start with something easy and encourage them to explore more about .NET.
    2. Dynamic Data is not really "fast food". Jika tahu cara "memakan"nya, kita bisa "sehat" dengan Dynamic Data :) Dynamic Data is actually a framework di atas ASP.NET, yang bisa fully customized melebihi yang kita bayangkan. Dynamic Data juga bisa diintegrasikan dengan existing web site.

    Beberapa contoh ASP.NET Dynamic Data customization, sekaligus capture dari sample yang saya deliver.

    Default:

    image

    Customized:

    image

    Default:

    image

    Customized:

    image

     

    Berikut foto-foto selama training. (Sorry atas kesalahan tanggal pada foto, seharusnya 06/26/2008)

    100_1775

    100_1776

    This is me...with moustache now. Ceritanya mau numbuhin beard biar bisa kayak Tony Stark :P

    100_1777

    Peserta training
    (left to right: Risgiyanto dari BAPESITELDA Jabar, Bela Negara dari Dinas Peternakan Jabar, Susan dan {waduh...punten kang, abdi hilap namina} dari BAPEDA Tasikmalaya)

     

    Slide dan sample code training ini dapat di-download di sini.

    Mudah2an training ini bisa men-trigger teman-teman kita di government untuk meng-explore lebih jauh dan mengimplementasikan .NET and related technologies sebagai pilihan platform development mereka. Berdasarkan pengalaman saya yang pernah exist di Pemda selama kurang lebih 3 tahun (2004 - 2007), banyak Satuan Kerja Pemerintah Daerah (SKPD), terutama Jawa Barat, lebih memilih open source technologies (PHP, Java) sebagai platform development. Melalui event training GCOE ini, let re-introduce them with the power that .NET can bring to life :)

    Share this post: | | | |
  • SQL Server-based SiteMap Provider

    Menanggapi pertanyaan di Milist dotnet (http://dotnet.netindonesia.net/?0::3593) tentang bagaimana membuat menu yang dynamically generated from database, rasanya gw perlu menulis posting ini. Mungkin ini topik lama, tapi demi mendokumentasikan knowledge, nggak ada salahnya gw posting di sini, ya gak?

    Jika yang dimaksud pertanyaan pada milist tsb adalah pemenuan untuk ASP.NET, bisa bertumpu pada control <asp:Menu> dengan attribut DataSourceID di-set dengan ID dari control <asp:SiteMapDataSource>. Kemudian, kita harus men-set attribut SiteMapProvider dari control SiteMapDataSource tsb dengan sebuah provider yang memungkinkan untuk meng-query element-element menu dari sebuah table di database. As far as I know, provider seperti itu belum ada di ASP.NET, so we need to create it.

    Ok, lets get started. In this case I use SQL Server database.

    1. Create a table and name it with whatever, such as: Sitemap

    image

    Then, insert some records, like this:

    image

    Please note the menu hierarchy. For example: menu with title Live Video is the child of Video menu because its PARENT is set to Video menu SITEMAP_ID

     

    2. Create provider class, name it SqlSiteMapProvider.cs, and place it inside App_Code folder. The code should be self-explained. I'll attach the code in this posting

    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Configuration.Provider;
    using System.Data;
    using System.Data.Common;
    using System.Data.SqlClient;
    using System.Runtime.CompilerServices;
    using System.Security.Permissions;
    using System.Web;
    using System.Web.Caching;
    using System.Web.Configuration;
     
    /// <summary>
    /// SqlSiteMapProvider
    /// This class is a site map provider implementation for site map data stored in SQL Server Database.
    /// This provider provides caching feature so that it will return root SiteMapNode if available in cache.
    /// </summary>
     
    namespace DyCode.Provider
    {
        [SqlClientPermission(SecurityAction.Demand, Unrestricted = true)]
        public class SqlSiteMapProvider : StaticSiteMapProvider
        {
            private const string ErrMsg1 = "Missing node ID";
            private const string ErrMsg2 = "Duplicate node ID";
            private const string ErrMsg3 = "Missing parent ID";
            private const string ErrMsg4 = "Invalid parent ID";
            private const string ErrMsg5 = "Empty or missing connectionStringName";
            private const string ErrMsg6 = "Missing connection string";
            private const string ErrMsg7 = "Empty connection string";
     
            public const string CacheDependencyName = "__SiteMapCacheDependency";
     
            private string m_ConnectionString;   // Database connection string
            private string m_TableName = "sys_sitemap";     // TableName that store site map
            private string m_CacheKey = "SQLSiteMapProvider_Nodes";
            private bool m_CacheEnabled = false;
            private int m_IndexID, m_IndexTitle, m_IndexUrl, m_IndexDesc, m_IndexRoles, m_IndexParent;
            private Dictionary<int, SiteMapNode> m_NodesDictionary = new Dictionary<int, SiteMapNode>(16);
            private readonly object m_Lock = new object();
            private SiteMapNode m_RootNode;
     
            public override void Initialize(string name, NameValueCollection config)
            {
                // Verify that config isn't null
                if (config == null)
                    throw new ArgumentNullException("config");
     
                // Assign the provider a default name if it doesn't have one
                if (String.IsNullOrEmpty(name))
                    name = "SqlSiteMapProvider";
     
                // Add a default "description" attribute to config if the
                // attribute doesn’t exist or is empty
                if (string.IsNullOrEmpty(config["description"]))
                {
                    config.Remove("description");
                    config.Add("description", "SQL Server site map provider");
                }
     
                // Call the base class's Initialize method
                base.Initialize(name, config);
     
                // Initialize m_ConnectionString
                string connect = config["connectionStringName"];
                if (String.IsNullOrEmpty(connect))
                    throw new ProviderException(ErrMsg5);
                config.Remove("connectionStringName");
     
                if (WebConfigurationManager.ConnectionStrings[connect] == null)
                    throw new ProviderException(ErrMsg6);
     
                m_ConnectionString = WebConfigurationManager.ConnectionStrings[connect].ConnectionString;
                if (String.IsNullOrEmpty(m_ConnectionString))
                    throw new ProviderException(ErrMsg7);
     
                // Initialize Table Name
                string tableName = config["tableName"];
                if (!String.IsNullOrEmpty(tableName))
                {
                    m_TableName = tableName;
                }
                config.Remove("tableName");
     
                // Initialize Cache Enabled/Disabled
                string cacheEnabled = config["cacheEnabled"];
                if (!String.IsNullOrEmpty(cacheEnabled))
                {
                    m_CacheEnabled = Convert.ToBoolean(cacheEnabled);
                }
                config.Remove("cacheEnabled");
     
                // Initialize Cache Key
                string cacheKey = config["cacheKey"];
                if (!String.IsNullOrEmpty(cacheKey))
                {
                    m_CacheKey = cacheKey;
                }
                config.Remove("cacheKey");
     
                // SiteMapProvider processes the securityTrimmingEnabled
                // attribute but fails to remove it. Remove it now so we can
                // check for unrecognized configuration attributes.
     
                if (config["securityTrimmingEnabled"] != null)
                    config.Remove("securityTrimmingEnabled");
     
                // Throw an exception if unrecognized attributes remain
                if (config.Count > 0)
                {
                    string attr = config.GetKey(0);
                    if (!String.IsNullOrEmpty(attr))
                        throw new ProviderException("Unrecognized attribute: " + attr);
                }
            }
     
            [MethodImpl(MethodImplOptions.Synchronized)]
            public override SiteMapNode BuildSiteMap()
            {
                lock (m_Lock)
                {
                    SqlConnection connection = new SqlConnection(m_ConnectionString);
                    SqlCommand command = null;
     
                    if (m_CacheEnabled)
                    {
                        SiteMapNode rootNode = (SiteMapNode)HttpRuntime.Cache.Get(m_CacheKey);
                        if (rootNode != null)
                        {
                            //check dependency
                            int count = 0;
                            try
                            {
                                command = new SqlCommand("SELECT COUNT(*) FROM " + this.m_TableName, connection);
                                command.CommandType = CommandType.Text;
                                connection.Open();
                                count = Convert.ToInt32(command.ExecuteScalar());
                            }
                            finally
                            {
                                connection.Close();
                            }
     
                            int siteMapNodeCount = Convert.ToInt32(HttpRuntime.Cache.Get(CacheDependencyName));
     
                            if (count != siteMapNodeCount)
                            {
                                //HttpContext.Current.Response.Write("Remove cache dependency<br/>");
                                //If table records that store sitemap are changed,
                                //remove the cache item so that OnSiteMapChanged event is triggered
                                HttpRuntime.Cache.Remove(CacheDependencyName);
                            }
                            else
                            {
                                //HttpContext.Current.Response.Write("Return SiteMapNode from cache<br/>");
                                return (SiteMapNode)HttpRuntime.Cache.Get(m_CacheKey);
                            }
                        }
                    }
     
                    // Make sure site map is cleared if it exists before continue             
                    if (m_RootNode != null)
                    {
                        ClearSiteMap();
                    }
     
                    // Query the database for site map nodes            
                    try
                    {
                        command = new SqlCommand("SELECT SITEMAP_ID, TITLE, DESCRIPTION, URL, ROLES, PARENT FROM " + this.m_TableName + " ORDER BY SITEMAP_ID", connection);
                        command.CommandType = CommandType.Text;
                        connection.Open();
     
                        SqlDataReader reader = command.ExecuteReader();
                        m_IndexID = reader.GetOrdinal("SITEMAP_ID");
                        m_IndexUrl = reader.GetOrdinal("URL");
                        m_IndexTitle = reader.GetOrdinal("TITLE");
                        m_IndexDesc = reader.GetOrdinal("DESCRIPTION");
                        m_IndexRoles = reader.GetOrdinal("ROLES");
                        m_IndexParent = reader.GetOrdinal("PARENT");
     
                        if (reader.Read())
                        {
                            // Create the root SiteMapNode and add it to the site map
                            m_RootNode = CreateSiteMapNodeFromDataReader(reader);
                            AddNode(m_RootNode, null);
     
                            // Build a tree of SiteMapNodes underneath the root node
                            int counter = 1;
                            while (reader.Read())
                            {
                                // Create another site map node and add it to the site map
                                SiteMapNode node = CreateSiteMapNodeFromDataReader(reader);
                                AddNode(node, GetParentNodeFromDataReader(reader));
                                counter++;
                            }
     
                            if (m_CacheEnabled && (HttpRuntime.Cache.Get(m_CacheKey) == null))
                            {
                                //Set a value for the cache entry that will serve as the 
                                //key for the dependency to be created on
                                HttpRuntime.Cache[CacheDependencyName] = counter;
     
                                //Create the array of cache key item names
                                string[] keys = new String[1];
                                keys[0] = CacheDependencyName;
     
                                //Create a dependency object referencing the array of cachekeys (keys)
                                CacheDependency dependency = new CacheDependency(null, keys);
     
                                HttpRuntime.Cache.Insert(m_CacheKey, m_RootNode, dependency,
                                                         Cache.NoAbsoluteExpiration,
                                                         Cache.NoSlidingExpiration,
                                                         CacheItemPriority.NotRemovable,
                                                         new CacheItemRemovedCallback(OnSiteMapChanged));
                            }
     
                        }
                    }
                    finally
                    {
                        connection.Close();
                    }
     
                    // Return the root SiteMapNode
                    return m_RootNode;
                }
            }
     
            private void ClearSiteMap()
            {
                Clear();
                m_NodesDictionary.Clear();
                m_RootNode = null;
            }
     
            protected override SiteMapNode GetRootNodeCore()
            {
                lock (m_Lock)
                {
                    BuildSiteMap();
                    return m_RootNode;
                }
            }
     
            // Helper methods
            private SiteMapNode CreateSiteMapNodeFromDataReader(DbDataReader reader)
            {
                // Make sure the node ID is present
                if (reader.IsDBNull(m_IndexID))
                    throw new ProviderException(ErrMsg1);
     
                // Get the node ID from the DataReader
                int id = reader.GetInt32(m_IndexID);
     
                // Make sure the node ID is unique
                if (m_NodesDictionary.ContainsKey(id))
                    throw new ProviderException(ErrMsg2 + "; Title: " + reader.GetString(m_IndexTitle));
     
                // Get title, URL, description, and roles from the DataReader
                string title = reader.IsDBNull(m_IndexTitle) ? null : reader.GetString(m_IndexTitle).Trim();
                string url = reader.IsDBNull(m_IndexUrl) ? null : reader.GetString(m_IndexUrl).Trim();
                string description = reader.IsDBNull(m_IndexDesc) ? null : reader.GetString(m_IndexDesc).Trim();
                string roles = reader.IsDBNull(m_IndexRoles) ? null : reader.GetString(m_IndexRoles).Trim();
     
                // If roles were specified, turn the list into a string array
                string[] rolelist = null;
                if (!String.IsNullOrEmpty(roles))
                    rolelist = roles.Split(new char[] { ',', ';' }, 4000);
     
                // Create a SiteMapNode
                SiteMapNode node = new SiteMapNode(this, id.ToString(), url, title, description, rolelist, null, null, null);
     
                // Record the node in the m_NodesDictionary dictionary
                m_NodesDictionary.Add(id, node);
     
                // Return the node        
                return node;
            }
     
            private SiteMapNode GetParentNodeFromDataReader(DbDataReader reader)
            {
                // Make sure the parent ID is present
                if (reader.IsDBNull(m_IndexParent))
                    throw new ProviderException(ErrMsg3 + "; Title: " + reader.GetString(m_IndexTitle));
     
                // Get the parent ID from the DataReader
                int pid = reader.GetInt32(m_IndexParent);
     
                // Make sure the parent ID is valid
                if (!m_NodesDictionary.ContainsKey(pid))
                    throw new ProviderException(ErrMsg4 + "; Title: " + reader.GetString(m_IndexTitle));
     
                // Return the parent SiteMapNode
                return m_NodesDictionary[pid];
            }
     
            public void OnSiteMapChanged(string key, object item, CacheItemRemovedReason reason)
            {
                lock (m_Lock)
                {
                    if (key == CacheDependencyName && reason == CacheItemRemovedReason.DependencyChanged)
                    {
                        // Clear the site map
                        ClearSiteMap();
                    }
                }
            }
        }
    }

     

    3. Create some configurations in web.config, like this:

    3.1. Make sure you have a connection string to be used to connect to database that contains the Sitemap table.

    <connectionStrings>
        <add name="SqlConnectionString" 
    connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=YOURDB;Integrated Security=True"/>
    </connectionStrings>

     

    3.2. Add SiteMap tag inside <system.web> tag

    <siteMap enabled="true" defaultProvider="AspNetSqlSiteMapProvider">
          <providers>
            <add    name="AspNetSqlSiteMapProvider" 
                    type="DyCode.Provider.SqlSiteMapProvider" 
                    securityTrimmingEnabled="true" 
                    connectionStringName="SqlConnectionString" 
                    tableName="Sitemap" 
                    cacheEnabled="true" 
                    cacheKey="MemberSiteMapProvider_Cache"/>
          </providers>
        </siteMap>

    If you notice, tableName attribute above is set with table name that we have created previously.

     

    4. In the page that will display menu, write this code:

    <asp:Menu id="SiteMenu" runat="server" DataSourceID="SiteMapDataSource1" 
    Orientation="Horizontal"/>
    <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" 
    SiteMapProvider="AspNetSqlSiteMapProvider" ShowStartingNode="false" />

    Please note that, SiteMapProvider attribute of SiteMapDataSource control is set with the name of provider that we have registered in web.config at step 3.

     

    5. Test it

    image

     

    This provider is what we DyCoders always use in many projects and no known problem so far.

    Some advantages of this provider are:

    1. Most of settings are configurable, like table name that stores menu data
    2. Cached. So it will not query menu data to db every time the menu displayed.

    In order to use other database, you should change all SQL Server-related stuffs (used ADO.NET classes, SQL statements, and created table) to desired database. You can adapt this code to use OR Mapping technology so database independency can be achieved.

    That's it. Enjoy. Please refer to attachment to get the complete code.

    Share this post: | | | |
    Posted Jun 13 2008, 01:49 PM by andriyadi with 5 comment(s)
    Filed under: