Patrice Calve

Life's short, have fun
posts - 46, comments - 33, trackbacks - 31

My Links

News

Archives

Post Categories

Image Galleries

Wednesday, November 12, 2008

ASP.Net MVC - inpractical web.sitemap in a dynamic context

 

The out-of-the-box StaticSiteMapProvider is great for, well, static web sites.  I don't find the StaticSiteMapProvider (and web.sitemap) model very practical for the dynamic nature of web sites/applications and especially Asp.Net Mvc applications.

In an mvc application it's difficult to render a static sitemap that allows breadcrumbs like:

  • Home
  • Home > Cars
  • Home > Cars > Porsche 911
  • Home > Cars > Porsche 911 > Edit

For the sake of discussion, and to keep the discussion as small as possible

  • Home: url = /default.aspx?
  • Cars: url = /Cars/Index (Controller=Cars, Action=Index)
  • Porsche 911: /Cars/View(id) (Controller=Cars, Action=View, id = id)
  • Edit: /Cars/Edit(id) (Controller=Cars, Action=Edit, id = id)

I'd like to have breadcrumb generating proper title (localized please) and url.  Maarten Balliauw wrote a nice MvcSitemapProvider where you can write a sitemap with dynamic.  What I don't like with the approach by Mr Balliauw is that I have to create a separate file that needs to keep be synched with the application, ie if the controller changes, I need to remember to change the sitemap.

So I'm offering you my "version" of a SiteMapProvider.  The angle I'm taking is to decorate classes and methods with an attribute and have a SiteMapProvider that uses builds the sitemap dynamically, using these attributes (with reflection).

I understand that reflection is slower than reading a static file, but from what I've found, the SiteMapProvider gets initialized once, on startup.  Ho, and I'm no expert by the way.

First, I created a blank, new AspNet Mvc (beta) application.  Then, I created 3 files:

  • AspNetMvcSiteMapNode.cs
  • AspNetMvcSiteMapProvider.cs
  • AspNetMvcSiteNodeAttribute.cs

We'll see them in details bellow, but first, let me show you how the "decoration" looks.  In the HomeController.cs, I decorated the "out-of-the-box" Index and About actions, and created another action called View,  Here a sample using the About and Item actions.

        [AspNetMvcSiteNode(Key = "HomeIndexAbout", Title = "About", Description = "Description of us", ParentKey = "HomeIndex", Url = "/Home/About")]

        public ActionResult About()

        {

            ViewData["Title"] = "About Page";

 

            return View();

        }

 

        [AspNetMvcSiteNode(Key = "HomeItem", Description = "An item, simple one", IsDynamic = true, ParentKey = "HomeIndex", Title = "Item {id}", Url = @"/Home/Item/\b(?<id>\d+)")]

        public ActionResult Item(int id)

        {

            SiteMap.CurrentNode.Title = string.Format("Item - foo[{0}]", id);

            ViewData["id"] = id;

            return View();

        }

My first "pass" at the attribute pattern above was to rely on the Provider to magically render the Title at run-time based on the "rawUrl" parameter, and a mix of title and DynamicUrl regex pattern.  It didn't turn out that well, more details at the end of the post.

So, instead of relying in the Provider, I decided to simply overwrite the node's Title myself in the actual "action".

SiteMap.CurrentNode.Title = string.Format("Item - foo[{0}]", id);

 With the "StaticSiteMapProvider", everything is, well, static... so the above doesn't work (pitty).  But with the AspNetMvcSiteMapNode provider, I made sure that SiteMapNodes are NOT readonly ;).

In the "Edit" action, I'm actually updating the "parentNode's" title !

        [AspNetMvcSiteNode(Key = "HomeItemEdit", Description = "Edit of the item, simple one", IsDynamic = true, ParentKey = "HomeItem", Title = "Edit", Url = @"/Home/Edit/\d+")]
        public ActionResult Edit(int id)
        {
            SiteMap.CurrentNode.ParentNode.Title = string.Format("Item - foo[{0}]", id);
            SiteMap.CurrentNode.ParentNode.Url = "/Home/Item/" + id;
 
            ViewData["id"] = id;
            ViewData["name"] = id.ToString();
            return View();
        }

 

The AspNetMvcSiteNodeAttribute.cs class is very basic:

    public class AspNetMvcSiteNodeAttribute : Attribute
    {
        public string Key { get; set; }
        public string Url { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string ParentKey { get; set; }
        public bool IsDynamic { get; set; }
        public bool IsRoot { get; set; }
    }

Nothing fancy.  The Key could actually be generated automatically, via a Guid, but it would be difficult to build the parent/child relationship with randomn data. 

I also created a AspNetMvcSiteMapNode.cs class, that inherits from the SiteMapNode and implements the "dynamic" portion.

    public class AspNetMvcSiteMapNode : SiteMapNode
    {
        /// <summary>
        /// If the url is dynamic (variable on the querystring, for example), set the value to True
        /// </summary>
        public bool IsDynamic { get; set; }
        public string DynamicUrl { get; set; }
        public string ParentKey { get; set; }
 
        public AspNetMvcSiteMapNode(SiteMapProvider provider, string key)
            : base(provider, key)
        {
            IsDynamic = false;
        }
    }

The Provider AspNetMvcSiteMapProvider.cs class, that inherits from the SiteMapProvider uses Reflection to get the AspNetMvcSiteNodeAttribute.  The algorithm includes a synchronization with the roles (via the AuthorizeAttribute). 

This is far from production ready code!!!!

    public class AspNetMvcSiteMapProvider : SiteMapProvider
    {
 
        private Dictionary<string, AspNetMvcSiteMapNode> _nodes;
        private AspNetMvcSiteMapNode _rootNode;
 
        public override SiteMapNode FindSiteMapNode(string rawUrl)
        {
            foreach (KeyValuePair<string, AspNetMvcSiteMapNode> kvp in _nodes)
            {
                if (kvp.Value.IsDynamic)
                {
                    Regex regex = new Regex(kvp.Value.DynamicUrl);
 
                    if (regex.IsMatch(rawUrl))
                    {
                        kvp.Value.Url = rawUrl;
 
                        int[] groupNumbers = regex.GetGroupNumbers();
 
                        Match match = regex.Matches(rawUrl)[0];
 
                        for (int i = 1; i < groupNumbers.Length; i++)
                        {
                            Group group = match.Groups[i];
 
                            kvp.Value.Title = kvp.Value.Title.Replace("{" + regex.GroupNameFromNumber(i) + "}", group.Value);
 
                        }
 
                        return kvp.Value;
                    }
                }
                else
                {
                    if (kvp.Value.Url.ToUpper() == rawUrl.ToUpper())
                    {
                        return kvp.Value;
                    }
                }
            }
            return null;
 
        }
 
        public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
        {
            SiteMapNodeCollection coll = new SiteMapNodeCollection();
 
            foreach (KeyValuePair<string, AspNetMvcSiteMapNode> kvp in _nodes)
            {
                if (kvp.Value.ParentKey != null && kvp.Value.ParentKey == node.Key)
                {
                    coll.Add(kvp.Value);
                }
            }
 
            return coll;
        }
 
        public override SiteMapNode GetParentNode(SiteMapNode node)
        {
 
            if (node != null && node.Key != null && node.Key != string.Empty && _nodes.ContainsKey(node.Key))
            {
                AspNetMvcSiteMapNode aNode = _nodes[node.Key];
 
                if (aNode.ParentKey != null && aNode.ParentKey != null && _nodes.ContainsKey(aNode.ParentKey))
                {
                    return _nodes[aNode.ParentKey];
                }
                else
                    return null;
 
            }
            else 
                return null;
 
 
        }
 
        protected override SiteMapNode GetRootNodeCore()
        {
            return _rootNode;
        }
 
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection attributes)
        {
            base.Initialize(name, attributes);
 
            _nodes = new Dictionary<string, AspNetMvcSiteMapNode>();
 
            Assembly a = Assembly.GetExecutingAssembly();
 
            foreach (Type t in a.GetTypes())
            {
                Attribute[] allAttributes = (Attribute[])t.GetCustomAttributes(typeof(AspNetMvcSiteNodeAttribute), true);
 
                foreach (Attribute att in allAttributes)
                {
                    if (att.GetType() == typeof(AspNetMvcSiteNodeAttribute))
                    {
                        addMvcNodeFromAttribute((AspNetMvcSiteNodeAttribute)att, null);
                    }
                }
 
                foreach (MethodInfo mi in t.GetMethods())
                {
                    foreach (Attribute att in mi.GetCustomAttributes(true))
                    {
                        if (att.GetType() == typeof(AspNetMvcSiteNodeAttribute))
                        {
                            addMvcNodeFromAttribute((AspNetMvcSiteNodeAttribute)att, mi);
                        }
                    }
                }
 
 
            }
 
 
        }
 
        private void addMvcNodeFromAttribute(AspNetMvcSiteNodeAttribute aspNetMvcSiteNodeAttribute, MethodInfo methodInfo)
        {
            AspNetMvcSiteMapNode node = new AspNetMvcSiteMapNode(this, aspNetMvcSiteNodeAttribute.Key);
            node.Title = aspNetMvcSiteNodeAttribute.Title;
            node.Description = aspNetMvcSiteNodeAttribute.Description;
 
            if (aspNetMvcSiteNodeAttribute.IsRoot)
                _rootNode = node;
            else
            {
                node.ParentKey = aspNetMvcSiteNodeAttribute.ParentKey;
            }
 
            node.ReadOnly = false;
 
            node.IsDynamic = aspNetMvcSiteNodeAttribute.IsDynamic;
            if (node.IsDynamic)
            {
                node.DynamicUrl = aspNetMvcSiteNodeAttribute.Url;
            }
            else
            {
                node.Url = aspNetMvcSiteNodeAttribute.Url;
            }
            if (methodInfo != null)
            {
                setNodeFromMethodInfo(methodInfo, node);
            }
 
            _nodes.Add(node.Key, node);
        }
 
        private static void setNodeFromMethodInfo(MethodInfo methodInfo, AspNetMvcSiteMapNode node)
        {            
            foreach (Attribute authAtt in methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), true))
            {
                if (authAtt.GetType() == typeof(AuthorizeAttribute))
                {
                    AuthorizeAttribute authorizeAttribute = (AuthorizeAttribute)authAtt;
 
                    string[] roles = authorizeAttribute.Roles.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
 
                    foreach (string role in roles)
                    {
                        node.Roles.Add(role);
                    }
                }
            }
        }
 
 
    }

Note that the AspNetMvcSiteNodeAttribute can be applied to any class.  For example, on the "Default.aspx.cs" class, I decorated the page_load method like this:

 

    public partial class _Default : Page
    {
        [AspNetMvcSiteNode(IsRoot=true,Key="Root", Url="/Default.aspx?", Title="Home", Description="The site's home page")]
        public void Page_Load(object sender, System.EventArgs e)
        {
            HttpContext.Current.RewritePath(Request.ApplicationPath);
            IHttpHandler httpHandler = new MvcHttpHandler();
            httpHandler.ProcessRequest(HttpContext.Current);
        }
    }

 

In the code above (and in the attribute), I have to specify the url.  I don't like that.  I really would like to forget about that "static" url and rely on the System.Web.Mvc to generate the proper urls in the case of controller/action methods.  But my attempts to make it work failed...

If the first page to load the web site in IIS is "/Default.aspx", then the HttpContext .Current.Handler is not the MvcHandler.    So I can't leverage the Routing.  If the first page loaded is handled by the MvcHandler, everything is fine.  Since the Provider's "initialize" gets fired once, at startup, I can't rely on the fact that it will always be the MvcHandler.

The HomeController.cs code is like this:

namespace MvcApplication1.Controllers
{
    [HandleError]
    [AspNetMvcSiteNode(Key="HomeController", Title="Home", Description="Home Page", Url="/Home", ParentKey="Root")]
    public class HomeController : Controller
    {
        [AspNetMvcSiteNode(Key="HomeIndex", Title="Index", Description="Description of Index", Url="/Home/Index", ParentKey="Root")]
        public ActionResult Index()
        { 
 
            for (int i = 0; i < 10; i++)
            {
                AspNetMvcSiteMapNode node = new AspNetMvcSiteMapNode(SiteMap.Provider, "HomeItem_" + i.ToString());
                node.Url = "/Home/Item/" + i.ToString();
                node.Title = string.Format("Item [id={0}]", i);
                node.IsDynamic = false;
 
                SiteMap.CurrentNode.ChildNodes.Add(node);
            }
            ViewData["Title"] = "Home Page";
            ViewData["Message"] = "Welcome to ASP.NET MVC!";
 
            return View();
        }
 
        [AspNetMvcSiteNode(Key = "HomeIndexAbout", Title = "About", Description = "Description of us", ParentKey = "HomeIndex", Url = "/Home/About")]
        public ActionResult About()
        {
            ViewData["Title"] = "About Page";
 
            return View();
        }
 
        [AspNetMvcSiteNode(Key = "HomeItem", Description = "An item, simple one", IsDynamic = true, ParentKey = "HomeIndex", Title = "Item {id}", Url = @"/Home/Item/\b(?<id>\d+)")]
        public ActionResult Item(int id)
        {
            SiteMap.CurrentNode.Title = string.Format("Item - foo[{0}]", id);
            ViewData["id"] = id;
            return View();
        }
 
        [AspNetMvcSiteNode(Key = "HomeItemEdit", Description = "Edit of the item, simple one", IsDynamic = true, ParentKey = "HomeItem", Title = "Edit", Url = @"/Home/Edit/\d+")]
        public ActionResult Edit(int id)
        {
            SiteMap.CurrentNode.ParentNode.Title = string.Format("Item - foo[{0}]", id);
            SiteMap.CurrentNode.ParentNode.Url = "/Home/Item/" + id;
 
            ViewData["id"] = id;
            ViewData["name"] = id.ToString();
            return View();
        }
 
    }
}

You have my code, so go ahead and play with it.  If you find improvements, let me/us know.

 

Regex in the DynamicUrl 

As mentionned above, my first "pass" at the attribute pattern above was to rely on the Provider to magically render the Title at run-time based on the "rawUrl" parameter, and a mix of title and DynamicUrl regex pattern.  But this idea only works if the value you want to show in the Title is the "id" ! 

  • Home > Cars [25]  // ok because id=25 is the value to show.
  • Home > Cars [Porsche]  // impossible because the provider can't render "Porsche" from the id 25...  so, problem 1

Problem 2, the "rawUrl" sent to the method FindSiteMapNode(string rawUrl) only works for the "current node", so the: Home > Cars [25] > Edit wouldn't be possible, because the "Cars [25]" portion would actually be rendered by the "parent" url being the "view", not the "edit".

So I kept the regex algorithm just in case it would be useful for someone someday.  Check the: public override SiteMapNode FindSiteMapNode(string rawUrl) Method from the Provider to see how I'm using it.

Have fun......  life's short.

Pat

posted @ Wednesday, November 12, 2008 9:48 PM | Feedback (2) |

Migrating VSS 2005 to TFS 2008

I finally and succesfully migrated a VSS 2005 Database to TFS 2008.

I got soo many problems/errors.  Things like:

  • Migration tool worked, but only the folders have been created in TFS. No file has been created,
  • Another migration warned that TF60085:  No file or folder to migrate
  • DCOM errors on the server.
  • When re-creating a TFS Project, TF30162: Task "WITs" from Group "WorkItemTracking" failed

So, after migrating "empty folders" the first time, I tried to delete the projects in TFS and re-importing them...  It turned to be a mess.........  I had to run the TFSDeleteProject.exe...

Here are my findings that may or may not be evident for mortals.

  • You must run the VSSConverter.exe from a "client" computer (not the TFS server)
  • The account you're using must have "project creation" priviliges on the TFS server
  • Check for DCOM errors on the server

If I had time and gutts, I'd start all over again (new os and all) and re-run everything with the following steps just to QA my steps!

To fix the DCOM errors,

Open Component Services

  • Start-->Administrative Tools-->Component Services
  • Expand Component Services, Computers, My Computer, DCOM Config.
  • Find the application (IIS WAMREG Admin Service / CLSID {61738644-F196-11D0-9953-00C04FD919C1}). 
  • Right-Click-->Properties and select the Security tab. 
  • For the "Launch and Activation Permissions, ensure that the Customize radio button is selected, and click Edit.
  • Add your service account (check the DCOM error message in the event viewer to find the right one), in my case, it was "NT AUTHORITY\NETWORK SERVICE"
    • Local Launch 
    • Remote Launch (not sure for this)
    • Local Activation
    • Remote Activation (not sure for this)
  • Restart IIS and continue on.

This "should" fix the DCOM errors in the future..  hehe.. 

As for the TF30162: Task "WITs" from Group "WorkItemTracking" failed.  I don't understand, but after "browsing" IIS console, re-starting IIS, I was able to re-create a new TFS Project without the TF30162 error above.  mmm.  maybe we just need to wait a few minutes (for sub-processes to finish/garbage collect) and a restart iis to clear caching. anywhoooo

ok, now back to migration

Perform the following steps from the client computer unless noted.

 

Step 1. Test Project creation on TFS

Open Visual Studio, connect to the TFS Server, try to create a project and upload a file to the Source Control.  If all works, continue, if not, fix !

 

Step 2: Analyze the VSS

Run the Analyze.exe.  Something like

"C:\Program Files\Microsoft Visual Studio\VSS\win32\ANALYZE.EXE" -f -c -d -v1 "d:\vss\data"

 

Step 3: get rid of checked outs files

There will likely be files that are checked out (older/defunct projects or un-monitored projects). 

  • Open VSS (client)
  • Search/Status Search
  • Choose "Display all checked out files"
  • Search Area "Search in in all projects"

If there are files found, send an email to your team or kill/"Undo Checkout" all files.

  • Open VSS client as admin
  • Right click the root "$/"
  • Choose Undo Checkout
  • Recursive = True, Local Copy = Leave, ok
  • Confirm all mesages... 

 

Step 4: install proper sps and hot fixes

if you run the VSSConverter and run into something like this:

Initializing...
VSSConverter has detected that Visual SourceSafe does not have the recommended u
pdates installed.  To ensure optimal results, install the updates referred to in
 Knowledge Base Article 950185.  Proceeding without these updates may lead to pr
oblems during migration.  Continue the migration without the updates (Y/N)?n

That's because the VSSConverter.exe needs to be updated (hotfix).

Well, kb950185, although the information seems to be correct, it wasn't clear that the hotfix can be found at  http://code.msdn.microsoft.com/ (the url is misleading, I think). 

Also, on that code.msdn page, to download the actual "exe", you have to click on "Current release"... Here's the direct link to the english (international) download page: http://code.msdn.microsoft.com/KB950185/Release/ProjectReleases.aspx?ReleaseId=1123

At the time of this writting, I have the following (all in English language, OSes and software):

Client:

  • Visual SourceSafe 2005 + Visual SourceSafe 2005 SP1
  • Visual Studio 2008 + Visual Studio 2008 SP1
  • TFS 2008 Explorer

Server:

- Team Foundation Server 2008 + TFS 2008 Explorer + TFS SP1

Note that all three (VSS client, TeamFoundationClient/Explorer and VSSConverter) must have the same language in order for VSSConverter to work.

Step 5: Run!

Follow the "standard" steps to migrate.  Here's my settings.xml file:

 

<?xml version="1.0" encoding="utf-8"?>
<SourceControlConverter>
  <ConverterSpecificSetting>
    <Source name="VSS">
      <VSSDatabase name="E:\VSS\Patware1.0" />
      <UserMap name="E:\tfs\usermapPatware1.0.xml"  />
    </Source>
    <ProjectMap>
      <Project Source="$/" Destination="$/Patware/"/>
    </ProjectMap>
  </ConverterSpecificSetting>
  <Settings>
    <!--<Output file="E:\tfs\analysisPatware1.0.xml"  />-->
     <TeamFoundationServer name="turner" port="8080" protocol="http"></TeamFoundationServer>
  </Settings>
</SourceControlConverter>

I "basically" used the same file for the analyze and the migrate portion.  I simply commented out the proper settings portion.

Glad it can help if it does!

Pat

posted @ Wednesday, November 12, 2008 2:46 PM | Feedback (1) |

Thursday, October 30, 2008

ASP.Net MVC Beta - ActionLink isn't generic anymore

Hi,

I just "migrated" from ASP.Net MVC Preview 5 to ASP.Net MVC Beta.  I uninstalled Preview 5 and installed the Beta.  I already had checked out previous previews but spent the most time with Preview 5, and didn't want to upgrade my "Preview-5-version-of-my-test-website" just in case there were deprecated or new features, specially in the "New ASP.Net MVC Application" template.  Gutt feeling.  So I created a new Project.

In Preview 5 release, the Html.ActionLink accepted a generic parameter list, with lambda expression, allowing to strongly-type the controller and it's method, including its parameters.  I really like this right now (my bubble may blow one day).

The syntax would look like this:

 <%= Html.ActionLink<CalveNet.Controllers.PatwareController>(c => c.Index(), "Here")%>

After creating my project, I copied a few files over (aspx pages, controllers, etc.).  But Visual Studio complained that the

"The non-generic method 'System.Web.Mvc.Html.LinkExtensions.ActionLink(System.Web.Mvc.HtmlHelper, string, string)' cannot be used with type arguments"

I found out that this generic/lambda version of the parameter is from a separate dll, called "ASP.NET MVC Beta Futures". 

To make it work, unzip the file, include the "Microsoft.Web.Mvc.dll" to your references, and don't forget to include the namespace in your web.config, under the path:

/configuration/system.web/pages/namespaces

add the following: <add namespace="Microsoft.Web.Mvc"/>

Pat

posted @ Thursday, October 30, 2008 8:09 PM | Feedback (8) |

Wednesday, October 15, 2008

CAB/SCSF Mocking Asynchronous Web Methods

Hi there,

How do you mock an asynchronous web method (web service) call?  You can skip the background and move on directly to the solution...

Background:

In a CAB/SCSF project I'm working on, I'm testing a Presenter's method "OnViewReady".  In my implementation, this OnViewReady does basically 2 things:

  1. Tells the View to Show a "Loading..." message to the user
  2. Issues an asynchronous call to a Web Service

When the call is returned from the Web Service, the Presenter tells the View to Show the message returned by the Web Service. 

The method looks like this: 

Solution

The solution is threefold:

  1. Make the Web Service Mocking Friendly ...
  2. Make the presenter "interface friendly" ...
  3. Fine tune the Test Method! :) ...

Web Service Mocking friendly

Making the Web Service Mocking friendly was not so evident.  Well, at least, the "mocking async methods" part.

To mock a Web Service, you have to extract an interface from the generated web service code (reference.cs), and make the web service code inherit from that interface.  You could make the web service "reference.cs" implement the interface, but may kill that code when you "update web reference".  The trick is to add a second "partial class" that will inherit from that interface.

Here goes:

  • In Visual Studio,
  • In your MyAppModule's project, click Add Web Reference and follow the wizard
  • Open the Reference.cs in the IDE (Show All Files)
  • Right Click on the public partial class MyWebService and choose "Refactor -> Extract interface"
  • This will create a IMyWebService interface with all of the methods from you Web Service.
  • Now, add a new class "MyWebService", make sure the namespace and class declaration is the same as the "real" web service.  "public partial class MyWebService", but make it implement the interface you created.

This is the web service's signature (in my example, the "MyWebService" is called "GeneralWS")

public partial class GeneralWS : System.Web.Services.Protocols.SoapHttpClientProtocol { .. }So nothing changed.

 

This is the interface extracted:

    public interface IGeneralWS

    {

        void CancelAsync(object userState);

        string Ping();

        void PingAsync(object userState);

        void PingAsync();

        event PingCompletedEventHandler PingCompleted;

        string Url { get; set; }

        bool UseDefaultCredentials { get; set; }

    }

This is the second partial class.  Note that there's no code, just the interface:

    public partial class GeneralWS : IGeneralWS

    {

        //look ma, no hands

    }

Now, if you open the reference.cs, check the PingCompletedEventArgs code: The constructor is "internral" !   So, this means that we can't mock the async method because we can't create a new PingCompletedEventArgs when we mimic the callback.

Again, what you do, is create an interface and leverage the "partial" declaration of the PingCompletedEventArgs.  Repeat the steps for the Web Service class, but this time, for the EventArgs.

Here are my results:

    public interface IPingCompletedEventArgs

    {

        string Result { get; }

    }

Add the second "partial" class:

    public partial class PingCompletedEventArgs : IPingCompletedEventArgs

    {

        public PingCompletedEventArgs(string results) :

                base(null, false, null)

        {

            //We're hard coding the results to an 1-item array (object) since we know

            //that the web service's Ping method returns a string.

            this.results = new object[1];

            this.results[0] = results;

        }

    }

Ok, that takes care of the "Web Service" part.  At this point, you can run the solution and it will work.  And if you update the web reference, you won't loose a thing.

Making the presenter "interface friendly"

Making the presenter "interface friendly" is the well-known "multiple constructor" algorithm.

In a classic web service call, your method will be like this:

        private void doSomethingWild()

        {

            GeneralWS gws = new GeneralWS();

            string returnValue = gws.Ping();

            View.ShowMessage(returnValue);

        }

In a "mocking friendly" class, you can't do that.  The class must work with an interface instead and the mocking framework will "mock" that interface for you.

4 steps:

  1. Add a private variable of type Interface (that you created earlier)
  2. Add a default constructor that will attach a "real web service" to that private interface variable
  3. Add a "mockig friendly" constructor where pass in in the mocked interface (this is the real trick)
  4. Change the call to the web service to not create a reference to the "real web service", but simply re-use the private variable of type interface

here's a sample from my presenter (construction part):

    public partial class HelloWorldViewPresenter : Presenter<IHelloWorldView>

    {

        private PeopleCentralGeneral.IGeneralWS _gws;

 

        //default parameter-less Presenter, for normal code execution

        public HelloWorldViewPresenter()

        {

            _gws = new GeneralWS();

            _gws.PingCompleted += new PingCompletedEventHandler(gws_PingCompleted);

 

        }

        //special constructor for "Mocking Friendlyness"

        public HelloWorldViewPresenter(IGeneralWS gws)

        {

            _gws = gws;

            _gws.PingCompleted += new PingCompletedEventHandler(gws_PingCompleted);

 

        }

        ...

        /// <summary>

        /// This method is a placeholder that will be called by the view when it has been loaded.

        /// </summary>

        public override void OnViewReady()

        {

            View.ShowMessage("Loading...");

            base.OnViewReady();

 

            pingAsync();

 

        }

        private void pingAsync()

        {

            //note that I'm not creating a new reference to the web service but re-using the interace instead..

            //proper coding would check that _gws isn't null, of course ;)

            _gws.PingAsync();

        }

 

        private void gws_PingCompleted(object sender, PingCompletedEventArgs e)

        {

            View.ShowMessage(e.Result);

        }

        ...

}

 

 

Fine tuning the Test Method

Fine tuning the Test Method for async web methods requires another trick with the Web Service.

Finally, the Test Method !

In the HelloWorldViewPresenterTest.cs, here's the Test Method for the OnViewReady:

 

        /// <summary>

        ///A test for OnViewReady ()

        ///</summary>

        [TestMethod()]

        public void OnViewReadyTest()

        {

            MockRepository repo = new MockRepository();

 

            //the mocked general web service

            IGeneralWS genWS = repo.StrictMock<IGeneralWS>();

 

            //the mocked view

            IHelloWorldView view = repo.StrictMock<IHelloWorldView>();

            Support.TestableRootWorkItem workitem = new Support.TestableRootWorkItem();

 

            //this will be used to simulate the call back from the web service

            Rhino.Mocks.Interfaces.IEventRaiser pingCompletedRaiser;

 

            using (repo.Record())

            {

                Expect

                    .Call(delegate { view.ShowMessage("Loading..."); });

 

                //Expect that the PingAsynch() method will be called

                Expect

                    .Call(delegate { genWS.PingAsync(); });

 

                //Provide the entry point for the call back.

                genWS.PingCompleted += null;

 

                pingCompletedRaiser = LastCall

                    .IgnoreArguments()

                    .GetEventRaiser();

 

                Expect

                    .Call(delegate { view.ShowMessage("Pong"); });

            }

 

            HelloWorldViewPresenter target = new HelloWorldViewPresenter(genWS);

            target.View = view;

            target.WorkItem = workitem;

 

            using (repo.Playback())

            {

                target.OnViewReady();

 

                //This "new EventArgs" would be impossible without the second partial class

                PingCompletedEventArgs args = new PingCompletedEventArgs("Pong");

 

                //make the callback call....  

                pingCompletedRaiser.Raise(genWS, args);

 

                //we're done!

            }

        }

I hope this info will help you in your quest for building better applications. 

Patrice Calvé

 

posted @ Wednesday, October 15, 2008 8:35 AM | Feedback (0) |

Tuesday, September 30, 2008

You know you're a Visual Studio User when...

You know you're a Visual Studio User when...   You're typing a document in Microsoft Word and you find yourself hitting CTRL-<space>+TAB to auto-complete a long word and/or you're not too sure of the spelling (in my case, it was hierarchical)!

posted @ Tuesday, September 30, 2008 1:28 PM | Feedback (0) |

Wednesday, May 28, 2008

Problem with .Net Web Services having values set incorrectly?

So,

You have a Web Service and a test client.  They (server + client) have been working great for months and all over sudden you're getting weird values.  The values sent from the server seem good, but the Xml/SOAP received doesn't get de-serialized correctly, and everything (or some properties) is set to default values (int=0, guid={00000000-0000-0000-0000-000000000000}, ...).  Read on !

The test client has a web reference to your Web Service.  A few web methods are returning objects (simple objects like Country(Id, NameEn, NameFr, etc.)).

When you run the Web Service from IE, the result is as expected:

  <?xml version="1.0" encoding="utf-8" ?>
- <ArrayOfCountry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://tempuri.org/People">
- <Country>
  <CountryId>1</CountryId>
  <NameEn>Canada</NameEn>
  <NameFr>Canada</NameFr>
  <ISOCode>CA</ISOCode>
  <Provinces />
  </Country>
  </ArrayOfCountry>

But for a reason or another, when you run it from your Test Client, every darn CountryId is set to 0.  If the the Id is a Guid, every row has a MyId = {00000000-0000-0000-0000-000000000000}.

Try updating your Web Reference(s)!  Right click the Web References -> Update Web Reference. 

Restart your test client.

How many times have I fallen in this trap? 

I hope this post will make me remember this mental note, and enlighten you!

Have fun, life's short

Pat

posted @ Wednesday, May 28, 2008 1:32 PM | Feedback (0) |

Thursday, May 01, 2008

I'm hangry at reunion.com

This morning, I received an invitation from a friend to join http://www.reunion.com/.

I like to discover new "networking" sites and the way they each have their own flavor.  So I joined in.

The registration is nice and works as expected for a "corporate application".   After all, a site that uses SSL and "TRUSTe CERTIFIED PRIVACY" must be in the right path, right?

The first thing I should have done is check the "web" for known privacy concerns or blogs that talk about it, but I didn't and shame of me, mea culpa.

Once registered I did what I like to do when I join a new "networking" site, use the contact discovery tool that most have.  As you may know, it's a functionality that downloads the contacts from another web site (gmail, hotmail, yahoo, linkedin, facebook, etc.) and offers you a few options.

The first thing most applications will do is tell you which downloaded contacts are already registered and offer you to "connect" with them.

The second thing most applications will do is offer you too choose the "un-registered" users in order to send them an invitation to join.  The better applications will give you the ability to customize the message these people will see.  It's particularly useful when the site isn't in the same language as the contacts you're inviting.  right?

Well, from my experience with reunion.com, the contact discovery utility is a virus or Trojan.  The site sent an email to everyone in my downloaded contact list without asking me first !

To everyone that received the reunions invitation on my behalf, I'm sorry.....  it won't happen again, trust me !

Shame on you reunion.com. 

1 minute after finding out this problem, I unregistered from the site, hopping that the site was not a spamming site disguised as ligit site.  Let's just say that I changed the password on the "contact list's web site" just in case.

Not impressed.

Pat

posted @ Thursday, May 01, 2008 11:28 AM | Feedback (1) |

Tuesday, March 11, 2008

Problem with a "No audio" audio/video?

So,  You've downloaded a video (screencast for example) and you can't hear a thing and you're running Vista?  You know that your "setup" is correct because you can play music.  You know that the video contains audio because you can see the oscilloscope/spectrum analyzer jump.  Yet, there's no sound.

If you look at the audio codec (file -> properties), it will probably say that the audio is something "mono"...

I bet you have a fancy 8 channel audio card with rear panel: front, rear, side, sub/center, line in, line out and front panel: mic, headphones.

I also bet you have either earphones or left/right speakers in the "front jacks" of the "rear panel".

Well....  depending on your audio card drivers, make sure to specify that you're setup is a "Stereo Audio Channel" and not a "Quadraphonic" or "5.1 Surround" !

Right click the Volume icon from the system tray (or Control Panel -> Classic View ->  Sound):



In the playback tab, you'll see a list of playback devices.  If there's only "speakers", chances are that you need to update the audio card device drivers (check your manufacturer).

If your default (green checkbox) is "Speakers", click configure and make sure that the "audio channel" is set to stereo:



A trick, to see what "playback device" is currently used by the audio/video program is running, press play on the audio/video program (winamp, windows media player, etc.), the selected playback device will have the sound level progress bar moving.

Of course, your setup (and/or problem) will be probably quite different from mine.  My computer is using an onboard RealTek audio card and I had to update my driver since I had a very old driver and couldn't do anything above with the old drivers.

It's "probably" not a codec issue, but a configuration issue.

Good luck...  don't pest.

posted @ Tuesday, March 11, 2008 3:19 PM | Feedback (7) |

Friday, February 29, 2008

absolutely useless fact

Both words "database" and "dataset" are written (on a qwerty keyboard) with the left hand !

that's it... nothing more

posted @ Friday, February 29, 2008 12:47 PM | Feedback (1) |

Wednesday, February 20, 2008

Virtual Server 2005 breaks Visual Studio 2008 WebServer on Vista

This weekend, I had this bright idea to install Virtual Server 2005 R2 on my Vista dev box at home to test a theory.

Well, it seems to break Visual Studio's WebServer.  When trying to run or debug an ASP.Net application, I was hit by a "WebDev.WebServer.exe has stopped working".

I tried many things, as you can imagine, but a few were:
- Reboot
- Un-installed Virtual Server 2005
- Un-installed/re-installed the Web Development Feature from the VS installation.
- Re-ran the various combinations of aspnet_regiis.exe

There was an entry in the Event Log's Application Logs:

Fault bucket 118476878, type 5
Event Name: CLR20r3
Response: None
Cab Id: 0

Problem signature:
P1: webdev.webserver.exe
P2: 9.0.0.0
P3: 4731664b
P4: System
P5: 2.0.0.0
P6: 471ebf0d
P7: 2c04
P8: 40
P9: System.Net.Sockets.Socket
P10:


System.Net.Sockets.Socket...   

I tried running webdev.webserver.exe manually from the command line:
C:\Program Files\Common Files\microsoft shared\DevServer\9.0
C:\Windows\Microsoft.NET\Framework\v2.0.50727

both worked (I tried port 8080 and 8888) (see note at the end)

Anyway, what seems to have fixed the problem was the following:

Disable IPv6 from your network adapters

Control Panel -> Network Connections -> for each adapter/connection: disable the TCP/IPv6

notes:
- When running the webDev exe manually, I didn't (and should have) try to run it with the same port that VS was trying with.
- I didn't try disabling IPv6 one connection at a time.  Maybe 1 did the trick, not sure.

Ho yes, one weird thing: the web based admin configuration page worked when the Web page launched by VS didn't

Anyway, I'm fixed for now and need to move on.

If this article helps you, woohoo...

If it doesn't, you'll find an answer in google ;)

Pat

posted @ Wednesday, February 20, 2008 8:49 PM | Feedback (4) |

Monday, February 11, 2008

Unit Testing = good

God I love writing Unit Tests.

I either find bugs in code, bugs in my unit tests (darn cut and pastes), or bugs in my head.  Let me explain in a second.

I like TDD.  I also like, TAD (Test After Development) or the one I use the most these days: TASAYC (Test As Soon As You Can)

The benefit of using TASAYC, is that you're creating unit tests about code you haven't touched in quite a while.  You either write the wrong unit tests because of "bugs in my head", the wrong unit tests because of assumptions of the ways the actual code is suppose to work (mix of "bugs in my head" + "bugs in code"), or you write the right unit tests and actually find bugs in the code!

Use TDD for those "normal test case testing".  ie: you write 1 or 2 "normal" unit tests for what your working on (procedure/function) and move on to the other areas of your application.   You end up with a "working application" quickly, and you have a "base" for unit testing (thus regression testing).

Before you "close" that class/form/app.  Go back to your unit test project and cram in those extra or "abnormal unit tests".  The extra unit tests are those that you know you should be testing but didn't have time and needed to fork out an application ASAP.  The "abnormal unit tests" are those where you to test the handling of improper inputs (overflows, etc.)

Remember, when you develop, the goal is the end product; ie: an application that others use, including QA/Testers.  Give them something to play with as soon as possible, then, while they are playing that application, improve the code by performing various methodologies:
  • Improve the Unit Testing
  • Scenario/Use Cases (actually testing of the app in a real life scenario)
  • Code Reviews

So,
  • No unit test = shame on you.
  • TDD = that's great, but don't get caught in writing more unit tests than applications!
  • Any other unit test methodology = That's great too!
  • A mix of both TDD + AnyOtherTestingMethodology = yeah baby.

Have fun, life's short!

Pat

posted @ Monday, February 11, 2008 4:07 PM | Feedback (1) |

Friday, June 15, 2007

Failed to access IIS metabase

Ha man !!!!

2 evenings lost trying to make this darn simple web app.  It's a Web Client Software Factory - June 2007 project on Vista box and whenever I was running the web app, I would be getting:

  Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Web.Hosting.HostingEnvironmentException: Failed to access IIS metabase.

The process account used to run ASP.NET must have read access to the IIS metabase (e.g. IIS://servername/W3SVC). For information on modifying metabase permissions, please see http://support.microsoft.com/?kbid=267904.

Source Error:

Server Error in '/DevelopmentWebsite' Application.

Failed to access IIS metabase.

 

Line 98:             else
Line 99:             {
Line 100:                configuration = WebConfigurationManager.OpenWebConfiguration(context.Request.ApplicationPath + "/" + configFilePath.Replace(context.Request.PhysicalApplicationPath, ""));
Line 101:            }
Line 102:            return configuration;


The solution is darn simple: run Visual Studio as an admin.  I changed my shortcut in my Quick Launch to "Run as Administrator".    Being an administrator on my machine wasn't enough, I had to be an admin admin!

The metaacl.vbs and adsutil.vbs aren't very usefull here as I'm running the Visual Studio WebServer.

Pat

posted @ Friday, June 15, 2007 8:18 PM | Feedback (1) |

Thursday, May 31, 2007

DRM + AACS

Hobbyists cracked (again) the AACS processing key.

Instead of spending years and futile millions on DRM, the entertainment industry should spend those resources on figuring out ways to produce products that are more interesting to buy than to steal.

It’s not a revolutionary idea…  Increase the cost of cigarettes, and up goes the quantity of contraband.  Increase the cost of alcohol and you’ll find more contraband.  Why do people buying stolen property?  Because the risk/cost of getting caught is far less than buying the real thing.

The only exception to this is gas prices, but that’s just until someone finds a solution.

No one wants to buy 10 songs for 25$, or a movie for 30$.

There’s no reason that a CD has to be sold at 25$ anyway.  How much is a blank CD?  1-10 cents?  How much money really goes to the artist 1-2 dollars?  Producing 1 CD or 25 million costs the same in terms of studio time, engineering, mixing, etc… 

Once the cost of producing the first CD has been recovered, the price of the CDs should go down, right?

Here’s a solution I’m giving away to the Entertainment Industry (for free, yes free!!!!)
- Cell at the price of 100$/each CD the first 1000 CDs, with a special casing, special cover, special printed/gold plated CD, manually autographed everything.
- Cell at the price of 25$/each CD the next 10 000 CDs, with a special “original owners” branded set
- Cell at the price of 10$/each CD the next 1 000 000 CDs… what we have today with cool cases and all
- At all time: cell at cost of producing  plain, basic, vanilla CD with simply the case and the CD: (2-4)$/each CD ???

No one will steal/rip a CD when it’s available at 2 $ !

Use the same idea for DVDs.

Pat

posted @ Thursday, May 31, 2007 9:49 AM | Feedback (1) |

Friday, March 09, 2007

Training on CAB/SCSF by David Platt

I'm finishing up my 4 day training course on CAB/SCSF by David Platt. 

It was a great class and encourage others to try it out.

Sir, if you're reading this: thanks again.

Pat

posted @ Friday, March 09, 2007 1:48 PM | Feedback (1) |

Thursday, March 08, 2007

Providing historical information to classes

Last year, I was working on a CAB/SCSF project that had to deal with concurrency (when a user tries to save changed data, but data on the server was already changed by someone else). 

We used optimistic concurrency and tried to auto-resolve the issues whenever possible and where the business rules allowed it.  Whenever auto-merge was not possible, we had to inform the user and provide a mechanism to allow the user to first discover the conflicts and choose the best answer.

Anyway, we had to deal with 3 information:

  1. The values changed by the user
  2. The original values
  3. The latest server values

On the client side, we kept 1 and 2 in separate variables:

Customer _customer;
Customer _customer_original;

Something trivial. 

However, I didn't like the fact that for each class, the original/updated state handling had to be handled by objects outside the class's scope (basic rule of thumbs).

The project was canned and never had the chance to work on a solution, until now...

I created a generic Historical<T> class that any business entity class could inherit. 

using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibrary1
{
   public class Historical<T>
   {
      /// <summary>
      /// Lorem Ipsum about the Original Value
      /// </summary>
      private T _original = default(T);
      private bool _initializing = true;
      private object lockObject = new object();
      internal bool Initializing
      {
         get { return _initializing; }
         set 
         {
            lock (lockObject)
            {
               _initializing =
value;
               if (_initializing == false)
                  makeACopy();
            }
         }
      }

      private void makeACopy()
      {
         if (_original == null)
            if (!_initializing)
               _original = (T)
this.MemberwiseClone();
      }

      public Historical()
      {
         _original =
default(T);
      }

      public T OriginalValues
      {
         get { return _original; }
      }

   }
}

Now, for the Business Entity Class, all I have to do is to inherit from this Historical class: 

public class Class1 : Historical<Class1>
{
   private string _name = "Not Set";

   public string Name
   {
      get { return _name; }
      set {_name = value; }
   }
}

I was presently supprised to see that it was possible (in C#) to declare a class "Class1" that inherits from a generic class "Historical" that referenced the child class!  Kind of a chicken and egg mystery broken !

Now, let's go back to the Historical<T> class, notice how the bool Initialized property is Internal.  This means that to benefit from the OriginalValues can only be given if the Initializing is set to false by an internal class.  This fits well in the CAB world where the Business Entity Classes are "pictures" of data on a server;  A piece of code needs to fetch the data from the server, create the Business Entity Class and fill in the values.

Here's an example of the data store class:

using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibrary1
{
   public static class
Store
   {
      public static Class1 GetClass1(int id)
      {
       
         //Get data from server and then set the values...

         Class1 c = new Class1();
         c.Name = "Pat";
         c.DateOfBirth = new DateTime(2000, 1, 2);

         //We're done with setting the values...
         c.Initializing = false;

         return c;
       }
   }
}

So, for a "client dev" point of view, the Class1 is very simple and easy to work with, allows modification to the data, while keeping track of the original values.  This "store" is a bad example

And quite frankly, we could go a step further and actually make the class1 partial, create a second "partial" class that inherits the Historical<T>.  This would make the original class1 "code generator friendly".

This strategy is CAB/SCSF friendly because:

  • The Business Entity class is easy to use from the GUI (add/update)
  • Adds benefits for handling concurrency
  • Non-intrusive

Wadaya think?

Pat 

posted @ Thursday, March 08, 2007 2:55 PM | Feedback (1) |

Powered by: