Update: I've updated the JavaScript to use the hoverIntent plugin to avoid unnecessary ajax calls when moving the mouse quickly over a list of items.

We're taking a bit of a break from WPF right now, so I'm back working on our ASP.NET project.

Our application is very person centric, so we wanted an easy way to pop up a "tooltip" with more information about a person whenever you see a person's name. For example, here are some search results. When you hover over a name, more information appears.

image

This is very much based on the Coda Popup Bubble example for jQuery that's been going around with a few important differences:

  1. The information is requested via AJAX, so you don't have to include all of this extra information in a hidden div. This keeps your markup smaller for grids with tons of names in it.
  2. When you mouse over another name, the previous one will disappear. If you tried this with the original Coda example, you'd end up with a weird streaking animation since there's a delay before the div is hidden.
  3. Works in IE (just turned off the fade animation)

Note that this requires jQuery 1.3.1. I found out that the new .live() functionality is very useful for ajax applications. Previously, if you bind a handler at startup, it will apply to elements that currently exist. The new live() functionality makes it to where you can apply events to elements that are created in the future. This means I can create a new link via ajax and it will still work with the hover tooltip.

Markup

All you need to do is specify a certain css class that jquery expects, and we're using the "rel" tag to contain data to pass to our ajax helper page.

<a class="personPopupTrigger" href="<link to person>" rel="4218,a17bee64-8593-436e-a2f8-599a626370df">House, Devon</a>
<a class="personPopupTrigger" href="<link to person>" rel="4218,f6434101-15bf-4c06-bbb2-fbe8c111b948">House, Gregory</a>

JavaScript

Run this on startup, and it will convert all of your links with the "personPopupTrigger" class to have the tooltip.

The container is the global container for the content. It's repositioned whenever a new link is hovered over.

$(function()
{
  var hideDelay = 500;  
  var currentID;
  var hideTimer = null;

  // One instance that's reused to show info for the current person
  var container = $('<div id="personPopupContainer">'
      + '<table width="" border="0" cellspacing="0" cellpadding="0" align="center" class="personPopupPopup">'
      + '<tr>'
      + '   <td class="corner topLeft"></td>'
      + '   <td class="top"></td>'
      + '   <td class="corner topRight"></td>'
      + '</tr>'
      + '<tr>'
      + '   <td class="left">&nbsp;</td>'
      + '   <td><div id="personPopupContent"></div></td>'
      + '   <td class="right">&nbsp;</td>'
      + '</tr>'
      + '<tr>'
      + '   <td class="corner bottomLeft">&nbsp;</td>'
      + '   <td class="bottom">&nbsp;</td>'
      + '   <td class="corner bottomRight"></td>'
      + '</tr>'
      + '</table>'
      + '</div>');

  $('body').append(container);

  $('.personPopupTrigger').live('mouseover', function()
  {
      // format of 'rel' tag: pageid,personguid
      var settings = $(this).attr('rel').split(',');
      var pageID = settings[0];
      currentID = settings[1];

      // If no guid in url rel tag, don't popup blank
      if (currentID == '')
          return;

      if (hideTimer)
          clearTimeout(hideTimer);

      var pos = $(this).offset();
      var width = $(this).width();
      container.css({
          left: (pos.left + width) + 'px',
          top: pos.top - 5 + 'px'
      });

      $('#personPopupContent').html('&nbsp;');

      $.ajax({
          type: 'GET',
          url: 'personajax.aspx',
          data: 'page=' + pageID + '&guid=' + currentID,
          success: function(data)
          {
              // Verify that we're pointed to a page that returned the expected results.
              if (data.indexOf('personPopupResult') < 0)
              {
                  $('#personPopupContent').html('<span >Page ' + pageID + ' did not return a valid result for person ' + currentID + '.<br />Please have your administrator check the error log.</span>');
              }

              // Verify requested person is this person since we could have multiple ajax
              // requests out if the server is taking a while.
              if (data.indexOf(currentID) > 0)
              {                  
                  var text = $(data).find('.personPopupResult').html();
                  $('#personPopupContent').html(text);
              }
          }
      });

      container.css('display', 'block');
  });

  $('.personPopupTrigger').live('mouseout', function()
  {
      if (hideTimer)
          clearTimeout(hideTimer);
      hideTimer = setTimeout(function()
      {
          container.css('display', 'none');
      }, hideDelay);
  });

  // Allow mouse over of details without hiding details
  $('#personPopupContainer').mouseover(function()
  {
      if (hideTimer)
          clearTimeout(hideTimer);
  });

  // Hide after mouseout
  $('#personPopupContainer').mouseout(function()
  {
      if (hideTimer)
          clearTimeout(hideTimer);
      hideTimer = setTimeout(function()
      {
          container.css('display', 'none');
      }, hideDelay);
  });
});

CSS

#personPopupContainer
{
    position:absolute;
    left:0;
    top:0;
    display:none;
    z-index: 20000;
}

.personPopupPopup
{
}

#personPopupContent
{
    background-color: #FFF;
    min-width: 175px;
    min-height: 50px;
}

.personPopupPopup .personPopupImage
{
    margin: 5px;
    margin-right: 15px;
}

.personPopupPopup .corner 
{
    width: 19px;
    height: 15px;
}
    
.personPopupPopup .topLeft 
{
    background: url(../images/personpopup/balloon_topLeft.png) no-repeat;
}
    
.personPopupPopup .bottomLeft 
{
    background: url(../images/personpopup/balloon_bottomLeft.png) no-repeat;
}
    
.personPopupPopup .left 
{
    background: url(../images/personpopup/balloon_left.png) repeat-y;
}
    
.personPopupPopup .right 
{
    background: url(../images/personpopup/balloon_right.png) repeat-y;
}
    
.personPopupPopup .topRight 
{
    background: url(../images/personpopup/balloon_topRight.png) no-repeat;
}
    
.personPopupPopup .bottomRight 
{
    background: url(../images/personpopup/balloon_bottomRight.png) no-repeat;
}
    
.personPopupPopup .top 
{
    background: url(../images/personpopup/balloon_top.png) repeat-x;
}
    
.personPopupPopup .bottom 
{
    background: url(../images/personpopup/balloon_bottom.png) repeat-x;
    text-align: center;
}

Images

Download

Ajax Helper Page

I'm not going to include a code sample for this since it's very specific to our application. It basically accepts query string parameters of "page" and "guid" and spits out details about that person.

Note: my example expects the ajax helper to return the content in a div with a css class of 'personPopupResult'. Also, it returns the person's guid in the result to verify that the returned result is for the same person currently selected since an ajax request might take a while in some cases and return an old result.

46

View comments

  1. Why not use a pageID attribute and a currentID attribute instead of hijacking the rel attribute?

    ReplyDelete
  2. @configurator no real reason... it just didn't feel right to add non-standard html attributes. Easy enough to modify the code to use named attributes for data.

    ReplyDelete
  3. Probably should try and cancel any running ajax call, before making a new one.

    Also you could check to see if the information is already loaded before calling again. However if its cached, shouldn't be an issue..

    ReplyDelete
  4. @Anonymous Can you provide a link on how to cancel an ajax call in jQuery? I looked a little bit earlier but didn't see an easy way to do it.

    ReplyDelete
  5. @Caleb, Try something like

    var x;
    if(x) {x = null; x.abort(); }
    x = $.ajax({ type: 'GET', url: aspx'});

    ReplyDelete
  6. sorry,
    var x;
    if(x) {x.abort(); x = null; }
    x = $.ajax({ type: 'GET', url: aspx'});

    ReplyDelete
  7. @Anonymous thanks for the tip, I'll give that a try

    ReplyDelete
  8. what html is your personajax.aspx page returning?

    ReplyDelete
  9. @Andrew It's returning html that has the content wrapped inside of a div with a css class of 'personPopupResult'.

    Example:

    [div class="personPopupResult"]
    First Name: Caleb
    Last Name: Tucker
    etc, etc...
    [/div]

    (Blogger doesn't like HTML in comments)

    ReplyDelete
  10. very nice, thanks!

    ReplyDelete
  11. Thanks for great work. Could you create a sample for this, I don't know how to use it

    ReplyDelete
  12. Very nice!
    Could you create a sample for this?? I tried make one but does´nt worked

    ReplyDelete
  13. Could you send by email?

    tonis.04@gmail.com

    ReplyDelete
  14. Got a problem, I've got everything set up and can see the tooltip box pop up when hovered, however it's blank ( no data ) - when using firebug i can see that the call hits the script and is responding with data - just doesnt seem to be making its way to the content div - any ideas?

    ReplyDelete
  15. @Toni sorry, I'm not going to be able to create a sample as the code I'm using is part of my application. I don't have enough time to separate it and create a working sample.

    ReplyDelete
  16. @Jon you might want to modify the success: function after the ajax call. The code I posted expects the HTML to be formatted a certain way (a div with class "personPopupResult"). You could remove this or make your returned HTML contain that.

    ReplyDelete
  17. i like this script.
    Very good.
    My site uses it

    ReplyDelete
  18. do you have sample in php!

    Alidad

    ReplyDelete
  19. When trying to implement this in Adobe AIR i get following error message!
    TypeError: Value undefined does not allow function calls. When this function is initialized
    $('.personPopupTrigger').live('mouseover', function()..
    Any idea?

    ReplyDelete
  20. this is very nice but under asp.net, I wish you could also develop for php and ajax too. can you!

    AM

    ReplyDelete
  21. @Andy my example just uses ASP.NET to retrieve information from the server. You could easily replace PersonAjax.aspx with a reference to a PHP file on your server that returns a result.

    ReplyDelete
  22. I guess i have the same problem as feriz... firebug returns an error $(".personPopupTrigger").live is not a function...

    ReplyDelete
  23. Great work. Any chance the popup could follow (move with) the mouse pointer?

    ReplyDelete
  24. @Anonymous, we didn't want it to move with the mouse pointer so that some of the items (like email address are clickable. You should be able to tweak it to make it move with the mouse, though.

    ReplyDelete
  25. Dear All,

    Is any one ever used jsPdf.js,
    I like to implement those script at my page.
    but the sample that present at the website doesn't work. Can you guys help me please...

    If it's possible please send me at my mail boy.beans@gmail.com.

    Thanks for the help
    B

    ReplyDelete
  26. for those that had problems with live not a function, make sure you upgrade to jquery 1.3. live() was only introduced recently.

    ReplyDelete
  27. we've got it live on our site now.. have a look here:

    http://www.puntersparadise.com.au/kingtipping/

    See the little ? marks near the BUY buttons... that's it!

    ReplyDelete
  28. You are voted!
    Track back from WebDevVote.com

    ReplyDelete
  29. Great job on this.
    Is there a way to make the actual link clickable at the same time that the tooltip is applied?
    I noticed in your sample code you do specify a link in the element.

    ReplyDelete
  30. This is a nice piece. Thank you!
    But I am having a problem with IE6 where the transparency image is not working properly. Has anyone find a fix for it?

    ReplyDelete
  31. I cant make this work in any way...what do i miss in here?
    Any live demo?
    Thanks...

    ReplyDelete
  32. I got it working ok but have a problem:
    Once the tooltip pops (mouseover) and then closes (mouseout) I click on other hyperlinks in my asp.net 3.5 page unrelated to this tooltip and I get redirected to a full screen page that displays the tooltip header. HOW Wierd?
    Is anyone else getting this problem? (bug?).

    ReplyDelete
  33. It would be really nice to have at least some basic demo setup. I cant figure out how to get it working... For example it says "Run this at startup" and i honestly have no idea what to run :-(

    ReplyDelete
  34. @ExtremeHobo "Run this at startup" means run the JavaScript code at the beginning of your page. e.g., create a [script type="text/javascript"][/script] block and put it in that.

    (Change [script] to proper html tag, of course)

    Sorry, I'm unable to put together a working sample.

    ReplyDelete
  35. Hi Celeb,

    I am getting the blank tooltip from the helper page eventhough I give the div class="personPopupResult".
    My helper page looks like this...

    |div class="personPopupResult"|
    First Name: Caleb
    Last Name: Tucker
    /div|

    I am calling somepage.cfm as helper page and that page has got the above code. Can you please correct me if I m missing something here...I love this tooltip. But its returning blank.

    Pl...help
    -Champ

    ReplyDelete
  36. @Champ, my code expects the person's ID to also be in the returned html somewhere as a hidden variable since you can have multiple active ajax requests open at the same time. Look at the line "if (data.indexOf(currentID) > 0)" in the jQuery. That's probably where you're having trouble.

    ReplyDelete
  37. Thanks a lot for very quick response...Yeap..that worked...one more quick question...Its not working in IE. You said, turn off the fade animation. where can find that?
    I really appreaciate your help.
    -Champ

    ReplyDelete
  38. @Champ, I was just saying that the Coda Popup I based the code on (http://jqueryfordesigners.com/coda-popup-bubbles/) didn't work well in IE because it was doing a fade animation.

    I'm not having any trouble in IE with the popup. Maybe some old JavaScript is being cached in your browser.

    ReplyDelete
  39. Still, I couldnt make it work in IE. Even in other browsers, I had to code this way by removing find('.personPopupResult').
    I m giving this way
    var text = $(data).html();
    rather than
    var text = $(data).find('.personPopupResult').html();

    Here is code in my helper page.
    [cfoutput]
    [div id="personPopupResult"]
    Passed ID: #guid#
    [input type="hidden" name="currentID" value="#guid#">
    [/div>
    [/cfoutput>

    ReplyDelete
  40. if you have time, please help me

    ReplyDelete
  41. @Caleb,
    I messed around with it for a while and read a bit more on jQuery and got it working. I also messed around with your code and images and made it appear more like a speech bubble coming up from the pointer. Thanks for getting me started!

    ReplyDelete
  42. pon la fuente menso

    ReplyDelete
  43. Hi Caleb,
    Sorry, I don't get it.
    I can't make tooltip with data.
    I tried this one.. please correct my mistake

    [html]
    [a class="..." href=".." rel="1,2"]test[/a]

    [ajax html]
    [html]
    [body]
    [div = "personPopupResult"]
    firstname = MYFN
    lastname = MYLN
    [input type="hidden" name="currentID" value="2"]
    [/div]
    [/body]
    [/html]

    please correct my mistake.
    thanks

    monn

    ReplyDelete
  1. If you haven’t seen NCrunch, it’s really worth it. http://www.ncrunch.net/ I recently upgraded it and found a few new features so I figured I would blog about it for those who haven't used it. It basically a plugin that is configurable to watch for code changes and re-run tests as you write the code. This way you have constant feedback as you write the code. It also puts little dots (or lines, it’s configurable) and has a nice little window you can dock to show you code coverage and # of failing tests. Checkout my setup below.
    You’ll see the green lines on the left mean at least 1 passing test is covering that code. Gray lines are not being covered by any tests. Red means at least one test is failing. Red X is a line that is causing a test to fail (via an obvious exception in the example). Finally, yellow is a performance concern. You can configure it to turn yellow if any line of code is taking over x milliseconds during a unit test. Mine is set to 200ms and you can see the ViewBag call is trigging it. The lines are clickable and show you the tests that are related to that line of code as well as giving you a quick way to jump to each test. It’s really cool. You might have to tweak it for performance so that VS doesn’t slow down too much depending on your dev machine's specs. It’s totally configurable and you can go as far as only running “pinned” tests so you can choose which ones you care about while you are actively coding. Then running them all later once you’re done with your coding session.
    2

    View comments

  2. I recently had some slowdowns that I attributed to ReSharper.  On every file save, the machine would freeze for a good 1-2 seconds.  That gets old really quickly.  Disabling ReSharper fixes the issue, but a fellow developer had no issues with speed with all the same settings.  After fighting with it for a couple days I finally remember to try and turn off VisualHG, my source control plugin.  After turning it off, the problem is gone and I can re-enable ReSharper.  I would much rather have ReSharper than VisualHG.  I typically never use VisualHG, it’s just never been a problem before so I left it on.  I’m not sure which team is responsible (if any) for the conflict, but I hope this post helps someone out.

    If you use Visual Studio 2010 and have random slowdowns or aren’t impressed with the speed in general, you should be running Visual Studio PerfWatson.  Read more here. Developers owe it to themselves to run this plugin and report anything.  I’m hoping the IE9 feature that tells you which plugins are resulting in a slow startup time makes its way to Visual Studio as well.  That way the pressure for the best feature of any software, speed, is placed squarely on the developers of the plugins.

    1

    View comments

  3. If you need to setup a central mercurial server, follow these steps.

    http://www.sjmdev.com/blog/post/2011/03/30/setting-mercurial-18-server-iis7-windows-server-2008-r2.aspx

    The only thing I would advice against would be using the re-write module.  It has a query string length limitation of 2050 characters.  Mercurial uses, in my opinion, the query string in a bad way when pushing changesets.  It really should use the POST for any large amount of data, but it uses the query string currently for some part of the POST.  This causes IIS to return a 404.  The limitation is easily avoided by not using the rewrite module.  You just have to deal with ugly URLs.

    0

    Add a comment

  4. Recently I have discovered that WCF doesn’t support service-side timeouts.  For a great code example and explanation of behavior check out this post on my favorite QA site, StackOverflow.

    I know you’re probably thinking “don’t right code that runs too long” or something along those lines.  In high concurrency systems, any number of things could cause code to run abnormally long lengths of time.  Just think about DTS (Distributed Transactions) as well as 3rd party system integrations.

    The basic problem is as stated in the post, the code running on the server never stops.  This is the equivalent of IIS not having timeouts.  Ludicrous.  The current solution is to write error-prone thread abort code ourselves.  It gets even weirder with WCF as you have client timeouts you can set.  So you can set a nice 20 second client timeout and the client will get a nice fault saying the operation timed out.  Good right?  No.  The client has been disconnected, but the code is continuing on like nothing happened on the service.

    Well I know it probably doesn’t apply to a lot of people, but I needed somewhere to vent about it.

    0

    Add a comment

  5. note: If you have any other option besides trying to drag table rows, pretend this post doesn’t exist and use divs.

    If you have a need for drag and drop functionality, look no further than jQuery UI’s sortable plugin.  If you need to drag and drop table rows, you’ll need to help the plugin along to ensure your table looks correct when dragging rows around.  Luckily the plugin exposes an option called helper.  This function simply runs when a draggable item is being moved.  This code below is pretty nasty, but it makes sure your dynamic width table looks great when dragging.

     var tableRowHelper = function(e, ui) {
     ui.children().each(function() {
      var newWidth = $(this).css('width');
      if (newWidth == 'auto' || newWidth == '') {
       newWidth = $(this).width() + 'px';
      }
      $(this).css('width', newWidth);
     });
     return ui;
     }; 
    
    // usage
    $('#someId').sortable({
     helper: tableRowHelper 
    });
    0

    Add a comment

  6. In case you aren’t aware, there is a new .NET based CMS system that just had it’s first release.  Orchard.

    Orchard is a free, open source, community-focused project aimed at delivering applications and reusable components on the ASP.NET platform.

    I’ve been keeping my eye on this project for some time and almost replaced this very blog with it.  In the end I decided against it, but mostly because the blog platform is “solved”.  I don’t need to re-invent the wheel yet again.

    If you’re looking to familiarize yourself with ASP.NET MVC and see how it scales to a very complex project, this is a great example.  They use the latest bits of MVC 3, including razor as the view engine of choice.  Download Orchard and start browsing the source.  If you don’t care about the source and just want a good CMS, make sure to pick the web platform installer version, it’ll make setting it up a breeze.

    Developing for Orchard is pretty straight forward.  Every module is essentially a MVC Area.  They even created a command line utility to generate out the default module layout.  I really like some of the convention-based pieces like the data migrations.  If you haven’t done so already, read the documentation and follow through with some tutorials on creating modules.  It’s quite easy once you learn the lingo of the project.  It has a very clear separation of concerns in all the layers.  They also did a good job dogfooding their own module api with the default modules so browsing the existing modules is very useful.  By the way, if you don’t have visual studio, you can still develop modules.  Orchard uses a dynamic compilation process to compile source-only deployed modules.  This should help Orchard when it comes to a growing community, paving the way for the the Mono enthusiasts to pick it up.

    I’ll be looking to commit some modules in the not-so-distant future and I’ll post a how-to here when I do.

    0

    Add a comment

  7. Note: This is what works for us, you may decide this sounds insane. You are allowed that opinion ;)

    We recently made the switch to using Mercurial (Hg) in our office and I dare say productivity has been through the roof compared to what now appears to be an archaic way of source control, known as subversion.  I won’t go into why we decided on Mercurial in this post but rather how we are using it today and what rocks about it.

    The Workflow

    At our company we have many products under a single umbrella with many pieces of shared code and functionality between the products.  I imagine many companies have very similar setups.  We have around 8 developers divided into teams.  Each team has their own set of goals and features for their next release for their product.  As you can imagine, having this many developers working on a single codebase that shares a lot of core functionality can be a bit complex.  For this example I’ll setup a blank repository and use TortoiseHg’s visual tools to help show the progress as we move along.

    image

    As you can see above, we have a brand new repository with a single commit.  Now lest assume we have two teams and they both have features to develop simultaneously.  In this workflow, we branch per feature.  The branch also must be a named branch.  This is great for clarity purposes, but we also use this for another reason I’ll explain later. 

    This is where things diverge from a “normal” mercurial workflow.  Mercurial, by default, only allows one head per repository.  This is how most projects work and for good reasons.  There is only ever one release so there only needs to be one branch head. 

    TortoiseHg has an option called “Push New Branch” which we will use from both teams repositories.  As the name implies, it pushes named branches to the central repo.  This is important as we want to be able to see what the other teams are working on without pulling changesets directly from one of the team members.  This is also important for Continuous Integration.  We want each team to have a continuous build for their feature.  This means we want their changes to be able to be pushed to the server’s copy of the repository.  The CI is essentially setup to build all named branches. 

    Here is the updated workflow after a few changes from each team have been pushed to the central repo.  *note that a central repo could be a codeplex or bitbucket account but in our case is just a locally hosted mercurial server

    image.

    As you can clearly see, you have two distinct branches coming from the same origin.  You’ll notice we named the branches Bug-1 and Bug-2.  We named the branches this way for bug tracking purposes, you can name your branches however you like.  Let’s say Team2 is done with their feature.  Their next step is to check in their latest changes and close the branch.  This is an important step as mercurial stops tracking closed branches.  This becomes useful in CI scenarios as well, the CI will no longer monitor a closed branch.  You can access the close branch command via the tortoise GUI using the branch button in the commit window.

    image

    Once the branch is closed and feature is complete, the team can merge the change back into default.  If a team needs to go back to the branch, they can.  Any commits will open the named branch automatically.

    image

    The other team is left finishing their bug and then merging in their changes in the same manner.

    image

    Now your cases are done and your stable branch is ready to be built by your favorite CI and published for the world to see!

    Why bother?

    There are many ways to deal with branches in mercurial and this is just the one we settled on.  A lot of people use “branches as clones” or “branching with bookmarks”.  This is a variation on “branching with named branches”.  Steve Losh has some great source control posts on his blog and I highly recommend reading them all before making a decision.

    There are some of the reasons we chose this workflow

    • Organization and Visibility
      • It’s easy to look at the list of branches to see what’s being worked on by all contributors.
        • If you use bug ids in branch names you can easily search all branches in the future to help find the code your looking for.
          • Implicit branches don’t have any identifying markers besides the commit message so it’s hard to find them later when you need them.
      • A single clone of the repository means no folder-shuffling between branch switches.
        • This is huge for productivity and sanity.
    • Easily get other contributors involved
      • Since the unfinished features aren’t just sitting locally on a developer’s box, you can easily update to any branch and contribute without pulling from a developer’s clone.
    • Backups
      • If you don’t allow force-push and don’t used named branches, backing up the main repo means only getting the main branch code in the backup.
      • Developers can constantly push their unfinished branches to the source server and it will get backed up in case of computer failure.
    • Continuous Integration
      • Products like TeamCity allow you to specify a branch name for the build you are setting up, this means each feature branch can have a build.  This would normally be impossible with implicit branches.
    • Works well with experimental feature branches.
      • Just close the branch if it doesn’t work out, no harm no foul.
    • You don’t have to use named branches for everything.  Small html fixes or something can easily be just committed directly to default if needed.
      • I highly recommend always using named branches though, they are virtually free and are little trouble for developers.
        • We all know how a small feature can turn into a month long project.
          • Creating a named branch allows for short term or long-term development with no side affects.

    Enforcing the workflow

    The workflow above is great, but mercurial is very flexible and doesn’t enforce such restrictions (rightfully so).  It allows developers to push implicit branches and multiple heads to a repo.  It doesn’t really “allow” it by default, but you can easily use the force push option and end up with a confusing situation for developers.  It even reminds the developer to do so in the error message.

    In our case, we want to enforce there there is only a single head per named branch.  For simplicity sake, we will consider default a named branch as well, it’s treated as such in most situations.  This makes things very clear for developers.  You can’t push your changes to the main repo without merging if your teammate has modified the code and beat you to the push. 

    Here is the full server plugin we use to enforce this rule.  As you can see, it’s quite simple.  This plugin runs before a push is finished.  So any developer that tries to push a changeset that contains more than one head on any branch would be rejected until the plugin passes the changesets.  Mercurial plugins are a whole topic themselves so I won’t go into how to go about setting it up expect for the bit of notes in the comments from the plugin.  If there is desire for such a tutorial in the future, I’ll write one up on the different ways of setting up mercurial and it’s plugins (client and server).

    #!/usr/bin/env python
    
    # Enforces that there is only a single head per named branch allowed
    
    # this hooks section should go in the hgrc of the repository you want the rules applied to 
    # [hooks] 
    # pretxnchangegroup.single_head = python://someserver/location/singlehead.py:single_head
    
    from mercurial.i18n import gettext as _
    
    def single_head(ui, repo, hooktype, node, **kwargs): 
        for b in repo.branchtags():        
            if len(repo.branchheads(b)) > 1: 
                ui.warn(_("-----------------------------------------\n")) 
                ui.warn(_("-----------------------------------------\n")) 
                ui.warn(_("Two heads detected on branch '%s'\n" % b)) 
                ui.warn(_("Only one head per branch is allowed!\n")) 
                ui.warn(_("Pull changes, merge and try again\n")) 
                ui.warn(_("-----------------------------------------\n")) 
                ui.warn(_("-----------------------------------------\n")) 
                return 1 
        return 0
    1

    View comments

  8. I'm not dead! Neither is Caleb, though I don't know if he'll begin blogging again. 

    The lack of new posts here is quite depressing really.  We both left the company we were previously with and moved on to greener pastures and the blogging stopped as we both ramped up at our new gigs.

    I'm going try to pump out at least one quality blog post a month and perhaps some smaller posts in between.  I've been diving into lots of new technology since I last posted and I'll try to remember the best bits and put them here.

    1

    View comments

  9. This code uses the excellent SocketsLight library to simplify socket connections in Silverlight.  I’ve put together a simple example of a chat server and client where you can send a message back and forth immediately.

    We plan to extend this idea to integrate with some hardware devices where the console application or Windows service connects to the hardware through drivers then we use sockets to communicate between that application and Silverlight.

    Download

    Download the example.

    Launching the Server

    1. Download the project and compile the solution. 
    2. Run LocalServer\bin\Debug\LocalServer.exe.  This is the console application that the Silverlight application connects to via sockets.  (Run this exe manually so you can F5 debug the Silverlight application.  You can’t debug both of them at the same time).

    image 

    Launching the Silverlight application

    1. Set the SilverlightSockets.Web project as the startup project and run.
    2. You should a message saying that the connection to the server succeeded.
    3. Type in a message and verify that the message appears on the server.

    image 

    Now you can type messages on the Silverlight client and on the server.

    Console application (LocalServer.exe)

    This is the console application that the Silverlight client connects to.  The PolicyServer and MessageServer classes are included in the SocketsLight library, and provide a simple event driven model for dealing with sockets.  The PolicyServer is just a simple web server that accepts requests to clientaccesspolicy.xml, which is necessary to use sockets in Silverlight.

    TextMessage is a class that I defined on both the server and the Silverlight client.  This stores data that’s sent over the wire in both directions.

    In this example, I’m firing up the PolicyServer and MessageServer on different threads and reading in any input typed in by the user.

    When a client connects, the ClientStatusChangedEventArgs contains a GUID that uniquely identifies the connection.  You use this GUID when you want to send a message to the client.  This example just supports a single connection, but you can easily keep track of the GUIDs for the different collections to support multiple connections.

    class Program
    {
        private static Guid _clientID = Guid.Empty;
    
        private static bool IsConnected
        {
            get { return _clientID != Guid.Empty; }
        }
    
        static void Main(string[] args)
        {
            var policyServer = new PolicyServer("../../clientaccesspolicy.xml");
            IMessageServer messageServer = new MessageServer(
                IPAddress.Any,
                4530,
                new JsonMessageSerializer(new List<Type>() { typeof(TextMessage) }));
    
            messageServer.ClientConnected += new EventHandler<ClientStatusChangedEventArgs>(messageServer_ClientConnected);
            messageServer.ClientDisconnected += new EventHandler<ClientStatusChangedEventArgs>(messageServer_ClientDisconnected);
    
            ThreadPool.QueueUserWorkItem((o) => { policyServer.Start(); });
            ThreadPool.QueueUserWorkItem((o) => { messageServer.Start(); });
    
            var textService = new TextService(messageServer);
    
            // Wait for messages to be typed and send to client.
            while (true)
            {
                string input = Console.ReadLine();
                if (IsConnected)
                {
                    messageServer.SendMessageToClient(_clientID, new TextMessage { Text = input });
                }
                else
                {
                    Console.WriteLine("Warning: there are not clients connected to receive the message.");
                }
            }
        }
    
        static void messageServer_ClientDisconnected(object sender, ClientStatusChangedEventArgs e)
        {
            _clientID = Guid.Empty;
            Console.WriteLine("Connection closed for " + e.ClientId);
        }
    
        static void messageServer_ClientConnected(object sender, ClientStatusChangedEventArgs e)
        {
            _clientID = e.ClientId;
            Console.WriteLine("Connection opened for " + e.ClientId);
        }
    }

    TextService.cs (runs on the console application)

    The data contract I defined between the server and client. It needs to inherit from the Message class included in the SocketsLight library.

    namespace LocalServer
    {
        using Witcraft.SocketsLight.Server;
        using System.Runtime.Serialization;
        
        [DataContract]
        public class TextMessage : Message
        {
            [DataMember]
            public string Text { get; set; }
        }
    }

    TextService.cs (runs on the console application)

    public class TextService
    {
        private IMessageServer _messageServer;
        Guid _clientID = Guid.Empty;
    
        public bool IsConnected
        {
            get { return _clientID != Guid.Empty; }
        }
    
        public TextService(IMessageServer messageServer)
        {
            _messageServer = messageServer;
            _messageServer.ClientConnected += new EventHandler<ClientStatusChangedEventArgs>(_messageServer_ClientConnected);
            _messageServer.ClientDisconnected += new EventHandler<ClientStatusChangedEventArgs>(_messageServer_ClientDisconnected);
            _messageServer.MessageRecieved += new EventHandler<MessageRecievedEventArgs>(_messageServer_MessageRecieved);
        }
    
        void _messageServer_MessageRecieved(object sender, MessageRecievedEventArgs e)
        {
            if (IsConnected)
            {
                TextMessage message = e.Message as TextMessage;
                if (message != null)
                {
                    Console.WriteLine("Message Received: " + message.Text);
                }
            }
        }
    
        void _messageServer_ClientDisconnected(object sender, ClientStatusChangedEventArgs e)
        {
            _clientID = Guid.Empty;
        }
    
        void _messageServer_ClientConnected(object sender, ClientStatusChangedEventArgs e)
        {
            _clientID = e.ClientId;
        }
    }

    Code-behind for Silverlight UserControl

    public partial class MainPage : UserControl
    {
        const int TCP_PORT = 4530;
        IMessageClient _messageClient;
    
        public MainPage()
        {
            InitializeComponent();
        }
    
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            _messageClient = new MessageClient
            (
                TCP_PORT,
                new JsonMessageSerializer(new List<Type>() { typeof(TextMessage) })
            );
            _messageClient.MessageRecieved += 
                new EventHandler<MessageRecievedEventArgs>(_messageClient_MessageRecieved);
            _messageClient.ConnectCompleted += 
                new EventHandler<AsyncCompletedEventArgs>(
                    _messageClient_ConnectCompleted);
            _messageClient.ConnectAsync();
    
            AddMessage("System", "Trying to connect on TCP port " + TCP_PORT + "...");
    
            SendText.Focus();
        }        
    
        void _messageClient_ConnectCompleted(object sender, AsyncCompletedEventArgs e)
        {
            AddMessage("System", "Connected!");
            this.Dispatcher.BeginInvoke(() => this.SendButton.IsEnabled = true );
        }
    
        void _messageClient_MessageRecieved(object sender, MessageRecievedEventArgs e)
        {
            TextMessage message = e.Message as TextMessage;
            if (message != null)
                AddMessage("Server", message.Text);
        }
    
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            SendMessage();
        }
    
        private void SendText_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
                SendMessage();
        }
    
        private void SendMessage()
        {
            AddMessage("Me", SendText.Text);
            _messageClient.SendMessageAsync(new TextMessage { Text = SendText.Text });
            SendText.Text = string.Empty;
        }
    
        // Updates UI with messages sent/received.
        private void AddMessage(string sender, string message)
        {
            this.Dispatcher.BeginInvoke
            (() =>
                {
                    this.Received.Text += string.Format("{0}: {1}", sender, message) + Environment.NewLine;
                    this.Scroller.UpdateLayout();
                    this.Scroller.ScrollToVerticalOffset(double.MaxValue);
                }
            );
        }        
    }
    2

    View comments

  10. I remember always being told to not return early from a method.  However, I think it makes sense in some cases when you would otherwise have a set of nested “if” statements.

    Take this example I ran across while working in our production system:

    private void GetDiscountCode(Registration registration, Registrant registrant)
    {
        if (registration.EventProfile.DiscountCodesEnabled &&
            registration.EventProfile.DiscountCodes.Count > 0)
        {
            if (tbDiscountCode.Text.Trim() != string.Empty)
            {
                DiscountCode discountCode = registration.EventProfile.DiscountCodes.Find(tbDiscountCode.Text);
                if (discountCode != null)
                {
                    if (discountCode.Active)
                    {
                        if (!discountCode.Limited
                            || discountCode.DiscountsUsed < discountCode.MaximumAllowed)
                        {
                            if (registrant.DiscountCodeApplied != discountCode.DiscountCodeId)
                            {
                                if (registrant.DiscountCodePercentage <= discountCode.DiscountPercentage)
                                {
                                    registrant.DiscountCodeApplied = discountCode.DiscountCodeId;
                                    registrant.DiscountCodePercentage = discountCode.DiscountPercentage;
                                    registrant.DiscountCodeAmountPerLineItem = discountCode.DiscountAmount;
                                    
                                }
                            }
                        }
                        else
                        {
                            errorMessages.Add(string.Format("The {0} you entered is no longer available", registration.EventProfile.DiscountCodeLabel));
                        }
                    }
                    else
                    {
                        errorMessages.Add(string.Format("The {0} you entered is not currently active", registration.EventProfile.DiscountCodeLabel));
                    }
                }
                else
                {
                    errorMessages.Add(string.Format("The {0} you entered is not valid for this event", registration.EventProfile.DiscountCodeLabel));
                }
            }
            else
            {
                registrant.DiscountCodeApplied = -1;
                registrant.DiscountCodePercentage = 0;
            }
        }
    }

    After refactoring (with the same behavior):

    private void GetDiscountCode(Registration registration, Registrant registrant)
    {
        if (!registration.EventProfile.DiscountCodesEnabled ||
            registration.EventProfile.DiscountCodes.Count == 0)
            return;
    
        string codeText = tbDiscountCode.Text.Trim();
        if (codeText == string.Empty)
        {
            registrant.DiscountCodeApplied = -1;
            registrant.DiscountCodePercentage = 0;
            registrant.DiscountCodeAmountPerLineItem = 0;
            return;
        }
    
        DiscountCode discountCode = registration.EventProfile.DiscountCodes.Find(codeText);
        if (discountCode == null)
        {
            errorMessages.Add(string.Format("The {0} you entered is not valid for this event",
                registration.EventProfile.DiscountCodeLabel));
            return;
        }
    
        if (!discountCode.Active)
        {
            errorMessages.Add(string.Format("The {0} you entered is no longer available",
                registration.EventProfile.DiscountCodeLabel));
            return;
        }
    
        if (discountCode.Limited && (discountCode.DiscountsUsed >= discountCode.MaximumAllowed))
        {
            errorMessages.Add(string.Format("The {0} you entered is no longer available",
                registration.EventProfile.DiscountCodeLabel));
            return;
        }
    
        if (registrant.DiscountCodeApplied != discountCode.DiscountCodeId)
        {
            if (registrant.DiscountCodePercentage <= discountCode.DiscountPercentage)
            {
                registrant.DiscountCodeApplied = discountCode.DiscountCodeId;
                registrant.DiscountCodePercentage = discountCode.DiscountPercentage;
                registrant.DiscountCodeAmountPerLineItem = discountCode.DiscountAmount;
            }
        }
    }

    I tend to think the refactored code is easier to read and maintain.

    0

    Add a comment

Labels
Blog Archive
Contributors
Contributors
Loading