Patrice Calve

Life's short, have fun
posts - 57 , comments - 90 , trackbacks - 31

Sunday, May 19, 2013

ReadOnlyObservableCollection

Use a ReadOnlyObservableCollection when you allow people to subscribe to changes to a collection, without allowing them to make changes to the collection.

ReadOnlyObservableCollection is a object from System.Collections.ObjectModel.

  • ReadOnly: Because there's no Add/Remove method
  • Observable: When an item is added or removed, it will fire an event (INotifyCollectionChanged).

But, if it's ReadOnly, how can it be Observable?  Because the object is actually a wrapper to a "real" collection.  When you create a new instance of this object, in the constructor, you need to specify a "real" collection.

So, you actually need two objects:

  1. A private, modifyable, observable collection
  2. A public, ReadOnlyObservableCollection

Here's a simple console app example to illustrate the use.  Persons is public but ReadOnly, whereas _myPeople is private, unexposed, but modifiable.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections.ObjectModel; namespace ConsoleApplicationForTestingCollections { class Program { static void Main(string[] args) { var ds = new DataService(); ds.Init(); // Note 1: here that we're using a "readonly" collection... Console.WriteLine(ds.Persons[0].FullName); Console.Read(); } } public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return string.Format("{0} {1}", FirstName, LastName); } } } public class DataService { public ReadOnlyObservableCollection<Person> Persons { get; private set; } private ObservableCollection<Person> _myPeople; public DataService() { _myPeople = new ObservableCollection<Person>(); this.Persons = new ReadOnlyObservableCollection<Person>(_myPeople); } public void Init() { // Note 2: here, we're adding an item in a private, unexposed, collection _myPeople.Add(new Person() { FirstName = "Foo", LastName = "Bar" }); } } }

Posted On Sunday, May 19, 2013 6:38 PM | Comments (0) |

Wednesday, December 12, 2012

1s Estimates

Estimation is easy: put any random number, just kidding.

Instead, always answer 1.  But, change the meaning of that 1.  Express your estimates in hours, days, weeks, months, seasons or quarters, years, decades, lifetime.

Humans are great at these kinds of estimates, and everyone can grasp the “order of magnitude” of such estimates.

How long will it take to implement feature x? 

1 day has a completely different implication than 1 year.

1 day (even if it takes 2 or even 3 days).  It means that technology is there, that the dev team knows what to do and how to do it.  Also, it means that a 100% slip in schedule equals 1 day, not one year.

1 year says a lot: you need to cut the feature in smaller chunks, you need a team, there will holidays, vacations, sick leaves, short leaves, pregnancy, the technology might not be there yet, there will probably research and development, proof of concepts, many changes in requirements as the project evolves and the client plays with the software, etc.  etc.  In a year, a 1% slip is equivalent to 1 week!

So, estimate using 1 and an “order of magnitude”.

Posted On Wednesday, December 12, 2012 7:54 AM | Comments (1) |

Thursday, August 16, 2012

RavenDb NoSql Sample Asp.Net MVC Embedded

RavenDB is an Open Source, document DB (NoSql)

Links:

- CodePlex: http://ravendb.net/

- WikiPedia on NoSql http://en.wikipedia.org/wiki/NoSQL

RavenDB can run as a server (standalone) or embedded in your application.

Here’s a very brief example of embedding RavenDb in an ASP.Net MVC application, it takes about 5-10 minutes in all.

 

Basic Setup Steps

  • Create an empty Asp.Net MVC Web Application
  • Install NuGet (if not already done)
  • Install RavenDb Embedded (will also install RavenDb Client)
  • Modify Global.Asax (this is where you will start the RavenDb embedded server)
  • That's it, press F5 to build and run....

Note: You “start” RavenDb (server) in the Global.asax by convenience.  Start and Stop of the server is process intensive and you want to do it only once.


Detailed Setup Steps

=> Modify Global.Asax (this is where you will start the RavenDb server)

1) Create a Property

public static DocumentStore Store { get; set; }

 

2)

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            Store = new EmbeddableDocumentStore { DataDirectory = "~/Store", UseEmbeddedHttpServer = true };
            Store.Initialize();

        }

 

 

Validation

There are two ways to verify the installation:

  1. through RavenDb's integrated Management Studio (simply navigate your browser to http://localhost:8080)
  2. or by code…

In this silly example, I'll modify the "HomeController" and the Index page.  Note, to fake some initial data to work with, I'll simply override the HomeController's constructor and save a dummy object to RavenDb if none exist... 


- Create a New RavenPerson Class (this will be my model)
- Override the constructor and store a sample record (just for the sake of testing)
- Override the Index() method to fetch the sample record and pass it to the view
- Override index.cshtml
- F5
 

 

Validation Details

 


=> Create a New RavenPerson Class (this will be my model)

namespace MvcApplicationForRavenDb.Controllers
{
    public class RavenPerson
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
    public class HomeController : Controller
    { …
=> Override the constructor and store a sample record (just for the sake of testing)

public HomeController()
        {
            using (var session = MvcApplication.Store.OpenSession())
            {
                var rp = session.Load<RavenPerson>("/Person/1");
 
                if (rp == null)
                {
                    rp = new RavenPerson();
                    rp.Id = "/Person/1";
                    rp.Name = "Pat";
                   
                    session.Store(rp,rp.Id);
                    session.SaveChanges();
                }
 
            }
 
        }           


=> Override the Index() method to fetch the sample record and pass it to the view

        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";
 
            using (var session = MvcApplication.Store.OpenSession())
            {
                var rp = session.Load<RavenPerson>("/Person/1");
 
                return View(rp);
            }
        }

 

=> Override index.cshtml

@model MvcApplicationForRavenDb.Controllers.RavenPerson
          
@{
    ViewBag.Title = "Home Page";
}
 
<h2>@ViewBag.Message</h2>
<p>
    To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
</p>
 
<p>The Raven Person should be [@Model.Name]</p>


- F5

 

Welcome to ASP.NET MVC!

To learn more about ASP.NET MVC visit http://asp.net/mvc.

The Raven Person should be [Pat]


Posted On Thursday, August 16, 2012 9:43 PM | Comments (0) |

Thursday, May 24, 2012

Sterling NoSQL Sample Console App

 

Jeremy Likness built a lightweight, NoSQL , object-oriented database for .Net 4.0 called Sterling.

First, wow.

Second, links:

- CodePlex: http://sterling.codeplex.com/

- WikiPedia: NoSql http://en.wikipedia.org/wiki/NoSQL

Third, here’s a very brief example of console application that uses Sterling, it takes about 5-10 minutes in all.

Here are the steps

  1. Create an empty Console Application
  2. install NuGet (if not already done)
  3. install Sterling
  4. Create a New Foo Class (this will be my model)
  5. Create New HelloDatabaseInstance Class (this will be my database)
    • Override the Name
    • Override the RegisterTables
  6. Modify the Main
  7. F5

 

Here are the details (Pardon my source code formatting…)

 

Create an empty Console Application

Capture001

I created an app called “ConsoleApplicationForTestingSterling”

 

 

Install NuGet (if not already done)

 

Install Sterling

Capture003

 

 

Create a New Foo Class (this will be my model)

Here’s the class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplicationForTestingSterling
{     
public class Foo     
{         
public int Id { get; set; }         
public string Name { get; set; }         
public bool IsBar { get; set; }         
public DateTime SomeDate { get; set; }     
}
}

 

 

Create New HelloDatabaseInstance Class (this will be my database)

The HelloDatabaseInstance is what you create to help Sterling know about the objects that you’ll be storing in the database (since Sterling is an Object Oriented Database, of course).

  • Must inherit from BaseDatabaseInstance
  • Override the Name
  • Override the RegisterTables

you basically create one “table” by base object that you’ll be storing in the database.

Person, Organization, Vehicle, Course, Order, etc.  Note that each object you’re storing can have sub-items, like an Order has OrderDetails, but you simple store Order and Sterling will make sure to store the OrderDetails too.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Wintellect.Sterling.Database;
 
namespace ConsoleApplicationForTestingSterling
{     
public class HelloDatabaseInstance : BaseDatabaseInstance     
{         
public override string Name         
{             
get             
{                 return "HelloDatabase";             
}         
}         
 
protected override List<ITableDefinition> RegisterTables()         
{             
return new List<ITableDefinition>             
{                 
CreateTableDefinition<Foo,int>(foo => foo.Id)             
};         
}     
}
}

 

 

Modify the Main

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Wintellect.Sterling;
 
namespace ConsoleApplicationForTestingSterling
{     
class Program     
{
static void Main(string[] args)         
{             
SterlingEngine v8 = new SterlingEngine();             
v8.Activate();             
var path = "Hello/"; // NOTE: the trailing slash "/" is important             
 
var driver = new Wintellect.Sterling.Server.FileSystem.FileSystemDriver(basePath:path);             
ISterlingDatabaseInstance db = v8.SterlingDatabase.RegisterDatabase<HelloDatabaseInstance>(driver);             
 
Console.WriteLine("Ready..");                                      
 
Foo f = new Foo();             
f.Id = 25;             
f.Name = "Twenty Five";             
f.IsBar = true;             
f.SomeDate = DateTime.Today;             
 
Console.WriteLine("Saving Foo 25");             
db.Save<Foo>(f);             
 
Console.WriteLine("Fetching Foo 25");             
 
Foo f25 = db.Load<Foo>(25);             
Console.WriteLine("Foo 25.Name = [{0}]",f25.Name);                          
 
v8.Dispose();             
 
Console.WriteLine("..Done");             
Console.ReadLine();         
}     
}
}

 

 

 

F5

Ready..
Saving Foo 25
Fetching Foo 25
Foo 25.Name = [Twenty Five]
..Done

Posted On Thursday, May 24, 2012 11:51 AM | Comments (2) |

Friday, January 29, 2010

Coding Standards and Fashion

Coding standards is to programming what walking is to a fashion show.

You can have the best coding standards and rules to force everyone to obey them, but if what you're building is crap, the walk won't help much.

Pat

Posted On Friday, January 29, 2010 6:49 AM | Comments (0) |

Wednesday, January 6, 2010

SCSF - Add View with presenter not showing

When creating a new SCSF project, the "solution recipe" does a great job creating the basis, but there's a few things you need to know/adjust.  Since creating SCSF solutions/projects isn't an activity we do on a day-to-day basis, we tend to forget these quirks.

The name you give your "solution" will be used in the assembly name (the output dll) and the namespace of the projects.  So the Infrastructure.Interface will have an assembly name "MySolution.Infrastructure.Interface.dll", and the namespace will be MySolution.Infrastructure.Interface.  The same will be for the MySolution.Infrastructure.Library, MySolution.Infrastructure.Module, MySolution.Infrastructure.Shell and MySolution.Infrastructure.Layout.

The problem is that the ProfileCatalog.xml is generated with assembly names without the "MySolution" prefix.  So, starting the solution will return a "ModuleLoadException was unhandled" error.

Don't change the values from the ProfileCatalog.xml.  Instead, change the AssemblyName to a dll name without the "MySolution" prefix, because...

When you later create a "business/fundation" project, the recipes like "Add View (with Presenter)" are expecting projects that reference the "Infrastructure.Interface.Dll"  If there's a prefix like "MySolution.Infrastructure.Interface.dll" the recipe/package won't be enabled.

 

Posted On Wednesday, January 6, 2010 8:14 PM | Comments (1) |

Thursday, May 28, 2009

Simple Sql script for finding a Guid/Uniqueidentifier in your database

So...

You're trying to debug an application that is using a large (100> tables) database and for a reason or an other, you don't understand where this darn Id is coming from!

Here's a simple Sql Script that browses your database for this darn Id...

Just change the @Id = '' for the id you're looking for!

declare @id as varchar(50)
 
--        idToRemove.ToString()    ""    String
 
 
select @id = '2eda82d8-9dcb-dd11-965a-001279d8c645'
 
set nocount on
 
 
 
declare @template as varchar(5000)
 
set @template = 'declare @id as uniqueidentifier
select @id = ''' + @id + '''
if exists(select * from [<%=TableName%>] where <%=ColumnNamesToInclude%>)
    begin
        print ''<%=TableName%>''
        select * from [<%=TableName%>] where <%=ColumnNamesToInclude%>
    end'
 
 
declare @TableName as varchar(250)
 
DECLARE TAB_CURSOR CURSOR FOR
    SELECT TABLE_NAME as TableName 
    FROM INFORMATION_SCHEMA.TABLES 
    WHERE TABLE_TYPE = 'BASE TABLE'
    ORDER BY TableName
 
OPEN TAB_CURSOR
 
FETCH NEXT FROM TAB_CURSOR
    INTO @TableName
 
WHILE @@FETCH_STATUS=0
BEGIN
 
    DECLARE @sql as varchar(max)
    DECLARE @columnCount as int
    DECLARE @colName as varchar(500)
    DECLARE @columnDataType as varchar(50)
    DECLARE @delimiter as varchar(10)
    DECLARE @columnNamesToInclude as varchar(max)
 
--    print '--------------------------------------------------------'
--    print '-- '
--    print '-- ' + @tableName 
--    print '-- '
--    print '-- '
 
    set @colName = ''
    set @delimiter = ''
    set @columnNamesToInclude = ''
    set @columnCount = 0
 
    DECLARE COL_CURSOR CURSOR FOR
        select COLUMN_NAME as [columnName]
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE 
            TABLE_NAME = @tableName
            AND DATA_TYPE = 'uniqueidentifier'
        ORDER BY ORDINAL_POSITION
 
    OPEN COL_CURSOR
 
    FETCH NEXT FROM COL_CURSOR
    INTO @colName
 
    WHILE @@FETCH_STATUS=0
    BEGIN
        set @columnCount = @columnCount + 1
        set @columnNamesToInclude = @columnNamesToInclude + @delimiter + '[' + @colName + '] = @Id'
 
        set @delimiter = ' OR ' 
 
        FETCH NEXT FROM COL_CURSOR 
            INTO @colName
 
    END
 
    CLOSE COL_CURSOR 
    DEALLOCATE COL_CURSOR  
 
    if @columnCount > 0 
    BEGIN
        set @sql = @template
 
        set @sql = replace(@sql, '<%=TableName%>', @tableName)
        set @sql = replace(@sql, '<%=ColumnNamesToInclude%>', @columnNamesToInclude)
        set @sql = replace(@sql, '<%=ScriptDateTime%>', getdate())
 
        exec (@sql)
--        print ''
--        print ''
--        print @sql
--        print ''
--        print 'GO'
--        print ''
--        print ''
--        print ''
--        print ''
 
 
    END
 
    FETCH NEXT FROM TAB_CURSOR 
        INTO @tableName
 
END
 
CLOSE TAB_CURSOR
DEALLOCATE TAB_CURSOR 

 

Posted On Thursday, May 28, 2009 2:29 PM | Comments (0) |

Wednesday, May 6, 2009

Very simple Log() method in VB

A long time ago, I wrote a very simple log method.  It was used to print the number of times a function/method was called.  I just changed it a bit and added an optional dumpAll parameter for printing the whole results.

It tracks 3 things: the name of the method, the number of times it was called, and stack level.

    Private Sub log(Optional ByVal dumpAll As Boolean = False)
        Static Dim h As New Generic.Dictionary(Of String, Integer)()
 
        If dumpAll Then
            For Each d As Generic.KeyValuePair(Of String, Integer) In h
                System.Diagnostics.Debug.Print("[{0}] was called [{1}] times", d.Key, d.Value)
            Next
        Else
 
            Dim st As New StackTrace(True)
            Dim m As System.Reflection.MethodBase = st.GetFrame(1).GetMethod()
 
            If Not h.ContainsKey(m.Name) Then
                h.Add(m.Name, 1)
            Else
                h(m.Name) = h(m.Name) + 1
            End If
 
            System.Diagnostics.Debug.Print("Method [{0}],(stack [{1}]),called [{2}] times", m.Name, st.FrameCount, h(m.Name))
 
        End If
    End Sub

 

All you need to do is to add the call to the log() method in your method/function.

Ex:

   Public Overrides Sub MyMethod()
        log()
        mSomeThing.DoSomethingEl()
    End Sub

It would be nice to have a macro/addin to add/remove this log() call to every method/function in a class.  Mmm...

Pat

Posted On Wednesday, May 6, 2009 9:47 AM | Comments (0) |

Wednesday, April 1, 2009

The Current state of Unit Testing in the software industry.

The Current state of Unit Testing in the software industry.

10 years ago, when the web boom started and applications were built with classic asp and ado, Unit Testing wasn't much on developers minds.

But, in 2009, I'm amazed by the lack of unit testing in most environments I've encountered...

In fact, most environments (not all, but most) I've seen don't even have a dedicated QA team, or even testers!!!

There are no excuses.

  • Visual Studio (since 2005) have right-click generation of unit test projects and test method stubs.
  • There are plenty of documentation, add ons/tools and frameworks.
  • Most Pattern & Practices (CAB/SCSF, Prism, Asp.Net Mvc, etc.) are greatly oriented towards unit testing.
  • We know now that the teams that include unit testing (especially TDD) are far more productive than teams that don't. (less time and better quality)


So, I blame:

  • Superiors/Bosses without a vision
  • Senior architects/developers without a vision or balls to push unit testing
  • Architects that don't show the way, or even block it (yeah, I know)
  • Project Leaders who didn't plan for Unit Testing in the "project plan"
  • Developers/programmers who don't care
  • Clients who accept mediocre applications
  • "The Media" who doesn't care because Unit Testing is not sexy and doesn't bring in the dough
  • A mentality of more "know-at-whatever-quality" instead of more "quality"
  • Demos that become applications
  • Sample Codes and snippets available on the web that hints away from Unit Testing
  • Projects that are either already started without unit testing or that start with a demo ware attitude
  • Microsoft for creating development tools that make it easier to write bad than it is to write good.

Visual Studio should have two template groups:

  1. Simple throw away templates (or templates that we have right now)
  2. Enterprise class templates

In 2) Enterprise templates, it should be impossible to write code that doesn't follow best practices like high availability, security, unit testability (if that word exists), for instance:

  • Tight Coupling should be "deprecated" !
  • The use of Interfaces should be mandatory ?
  • Ho and so much more

Since Visual Studio can throw a warning if a variable is used before it's set, if a variable is just not used, or if not all paths within a function return a value, lets include "hey, this is bad code".

In the next few years, we'll see development environments, frameworks and utilities that will make it easier to do right than it is to do wrong, making this blog obsolete, in the meantime, push the Unit Testing.

Finally,

I blame myself not working hard enough in promoting/evangelizing unit testing with my colleagues..

Mea Culpa

 

Pat

Posted On Wednesday, April 1, 2009 8:02 PM | Comments (2) |

Friday, March 13, 2009

How to display a Reporting Services Report in a Winform application

Step 1: prepare yourself :)

The first step to show a report on a windows application is to figure out the url and path of the report!

Sql Reporting Services has 2 virtual directories:

  • /Reports: renders the reports to the user in a user-friendly web page
  • /ReportServer: executes the report

When your boss asks you to display a report in a winform application, he(she) will likely send you a url in the following forms:

The urls above don't translate to properties that you need in a winform (or webform)...

To "consume" a report from a winform, you can't use the url above, you need another url.  Actually, you need two things:

  1. The Report Server Url
  2. The Path of the report

In the following two(2) steps, we'll see how to figure out how to discover that information.

Step 2: The Report Server Url

Ask your administrator, or find it for yourself!  The Reporting Server url is hidden in the report!

  • Open the report in a browser (ie)
  • Right click on the report's header (ex: on the report's title) -> View Source
  • Locate the meta tag "Report Server", the "Content" attribute will contain the Report Server value: ex
    • <META Name="Report Server" CONTENT="HTTP://ReportingServices/reportserver">

{ReportServerUrl} = "HTTP://ReportingServices/reportserver"

Step 3: The Report Path

The Report Path is something you have to "build" manually.

  • Open the ServerUrl in the browser (ie), a page with a list of folders will be shown
  • Copy the text of the folder into notepad or something like that. (referred bellow as {MyFolder})
  • Navigate to the folder, a page with a list of reports will be shown
  • Copy the text of the report into notepad: (referred bellow as {MyReportName})

The ReportPath will be the following: /{MyFolder}/{MyReportName}

ex: {ReportPath} = "/TMF Reports/TMF1200 - Project Status"

 

Step 4: The ReportViewer in the Winform

Now, you're ready to add the report on the winform.

  • Open the winform in Visual Studio
  • Add a ReportViewer on the designer (Toolbox > Data > ReportViewer)
  • Set the following properties:

Run the application.

Pat

Note To Microsoft

It would nice if it was easier to discover reports from within Visual Studio.  A wizard where a dev could enter any of the following:

Thanks, Pat

Posted On Friday, March 13, 2009 2:21 PM | Comments (5) |

Thursday, February 12, 2009

Musician and CD pricing, i wasn't too off, was I?

Well well well...

"Musician sells CD for $15.98, CD + live concert for $5000"  http://www.boingboing.net/2009/02/11/musician-sells-cd-fo.html

From John Wesley Harding's web site:

"Who was Changed and Who was Dead" 
is John Wesley Harding's first rock record since 2004.
It will be available in stores March '09.
But you can buy the album, not to mention download and hear it, immediately. There are various offers available to you.

I guess I wasn't too far off, was I ?  http://geekswithblogs.net/Patware/archive/2007/05/31/112885.aspx

Put a base price for a base article, and premium pricing for premium articles!

I still believe that John should have offered an option "download the album for x$ and get a certificate of ownership" proving that the customer did pay for the mp3. 

I don't bring my thousand CDs at work or on the bus.  I buy my CDs second hand, rip them to MP3, put them on an external hard drive, and enjoy what I paid for at work or at home.

My CDs are stored in a fireproof enclosure.

Pat

Posted On Thursday, February 12, 2009 7:58 AM | Comments (0) |

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 On Wednesday, November 12, 2008 9:48 PM | Comments (9) |

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 On Wednesday, November 12, 2008 2:46 PM | Comments (6) |

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 On Thursday, October 30, 2008 8:09 PM | Comments (10) |

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 On Wednesday, October 15, 2008 8:35 AM | Comments (2) |

Powered by: