Scott Hanselman

NuGet Package of the Week: A different take on ASP.NET MVC Forms with ChameleonForms

February 17, '15 Comments [35] Posted in ASP.NET MVC | NuGet | NuGetPOW | Open Source
Sponsored By

One of the nice things about any modular system (like ASP.NET) is the ability to swap out the parts you don't like. As the authors of ChameleonForms state, HTML forms is a pain. It's repetitive, it's repetitive, and it's boring. While ASP.NET MVC's Form Helpers help a lot, they felt that helper methods like Html.EditorForModel didn't go far enough or give you enough flexibility. ChameleonForms adds its own templating model and attempts to be as DRY as possible. It also takes a number of issues head on like better handling for drop-down lists and lists of radio buttons, and it even supports Twitter Bootstrap 3 to you can bang out HTML forms ASAP.

ChameleonForms also is a nice example of a tidy and well-run small open source project. They've got a public Trello backlog board, excellent documentation, a continuous integration build, a good example project, and of course, they're on NuGet. Check out the other projects that the folks in the "MRCollective" work on as well, as they've got their own GitHub organization.

NuGet Install ChameleonForms

Often ChameleonForms tries to use C# for the whole form, rather than switching back and forth from Div to Html Helper. For example:

@using (var f = Html.BeginChameleonForm()) {
using (var s = f.BeginSection("Signup for an account")) {
@s.FieldFor(m => m.FirstName)
@s.FieldFor(m => m.LastName)
@s.FieldFor(m => m.Mobile).Placeholder("04XX XXX XXX")
@s.FieldFor(m => m.LicenseAgreement).InlineLabel("I agree to the terms and conditions")
}
using (var n = f.BeginNavigation()) {
@n.Submit("Create")
}
}

This is the whole form using usings for scoping, and it's nice and clean.  How about a comparison example? Here's standard ASP.NET MVC:

@using (Html.BeginForm())
{
<fieldset>
<legend>A form</legend>
<dl>
<dt>@Html.LabelFor(m => m.RequiredString, "Some string")</dt>
<dd>@Html.TextBoxFor(m => m.RequiredString) @Html.ValidationMessageFor(m => m.RequiredString)</dd>
<dt>@Html.LabelFor(m => m.SomeEnum)</dt>
<dd>@Html.DropDownListFor(m => m.SomeEnum, Enum.GetNames(typeof(SomeEnum)).Select(x => new SelectListItem {Text = ((SomeEnum)Enum.Parse(typeof(SomeEnum), x)).Humanize(), Value = x})) @Html.ValidationMessageFor(m => m.SomeEnum)</dd>
<dt>@Html.LabelFor(m => m.SomeCheckbox)</dt>
<dd>@Html.CheckBoxFor(m => m.SomeCheckbox) @Html.LabelFor(m => m.SomeCheckbox, "Are you sure?") @Html.ValidationMessageFor(m => m.SomeCheckbox)</dd>
</dl>
</fieldset>
<div class="form_navigation">
<input type="submit" value="Submit" />
</div>
}

And here is the same form with ChameleonForms.

@using (var f = Html.BeginChameleonForm()) {
using (var s = f.BeginSection("A form")) {
@s.FieldFor(m => m.RequiredString).Label("Some string")
@s.FieldFor(m => m.SomeEnum)
@s.FieldFor(m => m.SomeCheckbox).InlineLabel("Are you sure?")
}
using (var n = f.BeginNavigation()) {
@n.Submit("Submit")
}
}

But these are basic. How about something more complex? This one has a bunch of variety, a number overloads and customizations, as well as a FileUpload (note that the form is a Multipart form):

@using (var f = Html.BeginChameleonForm(method: FormMethod.Post, enctype: EncType.Multipart))
{
<p>@f.LabelFor(m => m.SomeCheckbox).Label("Are you ready for: ") @f.FieldElementFor(m => m.SomeCheckbox) @f.ValidationMessageFor(m => m.SomeCheckbox)</p>
<p>@f.FieldElementFor(m => m.RequiredStringField).TabIndex(4)</p>
using (var s = f.BeginSection("My Section!", InstructionalText(), new{@class = "aClass"}.ToHtmlAttributes()))
{
using (var ff = s.BeginFieldFor(m => m.RequiredStringField, Field.Configure().Attr("data-some-attr", "value").TabIndex(3)))
{
@ff.FieldFor(m => m.NestedField).Attr("data-attr1", "value").TabIndex(2)
@ff.FieldFor(m => m.SomeEnum).Attr("data-attr1", "value")
@ff.FieldFor(m => m.SomeEnum).Exclude(SomeEnum.SomeOtherValue)
}
@s.FieldFor(m => m.SomeCheckbox).AsDropDown()
using (var ss = s.BeginSection("Nested section"))
{
@ss.FieldFor(m => m.FileUpload).Attr("data-attr1", "value")
}
@s.FieldFor(m => m.RequiredStringField).OverrideFieldHtml(new MvcHtmlString("Custom html <b>she-yeah</b>!"))
@s.FieldFor(m => m.TextAreaField).Cols(60).Rows(5).Label("Some Label").AutoFocus().TabIndex(1)
@s.FieldFor(m => m.SomeCheckbox).InlineLabel("Some label").WithHint("Format: XXX")
@s.FieldFor(m => m.SomeCheckbox).AsRadioList().WithTrueAs("True").WithFalseAs("False")
@s.FieldFor(m => m.ListId)
@s.FieldFor(m => m.ListId).AsRadioList()
@s.FieldFor(m => m.SomeEnums)
@s.FieldFor(m => m.SomeEnumsList).AsRadioList()
@s.FieldFor(m => m.Decimal)
@s.FieldFor(m => m.Int).AsInputGroup().Append(".00").Prepend("$")
@s.FieldFor(m => m.DecimalWithFormatStringAttribute)
@s.FieldFor(m => m.NullableInt)
@s.FieldFor(m => m.Child.ChildField)
@s.FieldFor(m => m.Child.SomeEnum).AsRadioList()
@s.FieldFor(m => m.RequiredStringField).Disabled()
@s.FieldFor(m => m.RequiredStringField).Readonly()
}
using (var n = f.BeginNavigation())
{
@n.Submit("Submit")
@n.Reset("Reset")
}
}

ChameleonForms also has a special NuGet package if you're using TwitterBootstrap that changes how forms with the BeginChameleonForm method render.

ChameleonForms also has some convenient extra abilities, like being able to automatically infer/create a [DisplayName] so you don't have to. If you're doing Forms in English and your preferred Display Name will end up just being your variable name this can be a useful time saver (although you may have opinions about its purity.)

So instead of the tedium of:

[DisplayName("Email address")]
public string EmailAddress { get; set; }

[DisplayName("First name")]
public string FirstName { get; set; }

You can just say this once, picking just one...this is an example where they use HumanizedLabels.

HumanizedLabels.Register(LetterCasing.AllCaps) => "EMAIL ADDRESS"
HumanizedLabels.Register(LetterCasing.LowerCase) => "email address"
HumanizedLabels.Register(LetterCasing.Sentence) => "Email address"
HumanizedLabels.Register(LetterCasing.Title) => "Email Address"

If you've got a lot of Forms to create and they're just no fun anymore, you should definitely give ChameleonForms a try. If you're a Twitter Bootstrap shop, doubly so, as that's where ChameleonForms really shines.

I'll do a few other posts exploring different ways to for Forms in ASP.NET MVC in the coming weeks. Be sure to explore the NuGet Package of the Week Archives as well!


PLUG: Did you know I have a YouTube channel? Subscribe over here. I've got tutorials on how to effectively use Windows 8 and 8.1, Build to Build walkthroughs of the latest versions of Windows 10, and I just started a new series I'm sure you'll want to share with your family called "How to REALLY use Microsoft Office." Help me out and spread the word!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web

NuGet Package of the Week: Polly wanna fluently express transient exception handling policies in .NET?

January 13, '15 Comments [13] Posted in NuGet | NuGetPOW
Sponsored By

LOL at my own post title. Pardon me.

Install-Package Polly

Michael Wolfenden has a very clever open source library called Polly. Polly is a .NET 3.5 / 4.0 / 4.5 / PCL library that allows developers to express transient exception handling policies such as Retry, Retry Forever, Wait and Retry or Circuit Breaker in a fluent manner.

Handling exceptions can be a hassle sometimes. Not just setting the try/catches up, but deciding on the policy for the catch can make the exception management code more complex than the method itself!

Polly has a fluent interface to make expressing rules like that much easier. For example:

// Single exception type
Policy
.Handle<DivideByZeroException>()

// Single exception type with condition
Policy
.Handle<SqlException>(ex => ex.Number == 1205)

// Multiple exception types
Policy
.Handle<DivideByZeroException>()
.Or<ArgumentException>()

// Multiple exception types with condition
Policy
.Handle<SqlException>(ex => ex.Number == 1205)
.Or<ArgumentException>(ex => x.ParamName == "example")

Then you can add Retry() logic, which is fantastic.

// Retry multiple times, calling an action on each retry 
// with the current exception and retry count
Policy
.Handle<DivideByZeroException>()
.Retry(3, (exception, retryCount) =>
{
// do something
});

Even do retries with multiplicative back off!

// Retry a specified number of times, using a function to 
// calculate the duration to wait between retries based on
// the current retry attempt, calling an action on each retry
// with the current exception, duration and context provided
// to Execute()
Policy
.Handle<DivideByZeroException>()
.WaitAndRetry(
5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timeSpan, context) => {
// do something
}
);

Once you have set up a policy, you execute on it.

Policy
.Handle<SqlException>(ex => ex.Number == 1205)
.Or<ArgumentException>(ex => ex.ParamName == "example")
.Retry()
.Execute(() => DoSomething());

Polly also supports the more sophisticated "Circuit Breaker" policy. For more information on the Circuit Breaker pattern see:

Circuit breaker tries and then "Trips the circuit breaker" so you'll get a BrokenCircuitException for some amount of time. This is a great way to give an external system to chill for a minute if it's down. It also externalizes the concept so that you could theoretically handle a down database the same as you handle a down external web API.

// Break the circuit after the specified number of exceptions
// and keep circuit broken for the specified duration
Policy
.Handle<DivideByZeroException>()
.CircuitBreaker(2, TimeSpan.FromMinutes(1))

You can explore the code, of course, up on Github in the Polly repository. Go give the Polly project a star. They've recently added async support for .NET 4.5 as well!


Sponsor: Welcome this week's feed sponsor, Stackify! - The only developer-friendly solution that fully integrates .NET error & log management with application monitoring. Easily isolate issues and focus your efforts – Support less, Code more. Free trial

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web

NuGet Package of the Week - Courtesy Flush to flush buffers earlier and optimize time to first byte

November 6, '14 Comments [24] Posted in ASP.NET | ASP.NET MVC | NuGet | NuGetPOW
Sponsored By

Yes, really. It's got to be the best name for an open source library out there. It's a great double entendre and a great name for this useful little library. Perhaps English isn't your first language, so I'll just say that a courtesy flush gives the next person a fresh bowl. ;)

However, in the computer world "flushing a buffer" means forcing a buffer to be moved along, usually to a file or the network. Rather than holding data, you flush it, and move it along.

Nik from Glimpse has a small NuGet package called Courtesy Flush. He's got a good write-up on his blog.

image

It's a library to enable easier flushing of your buffer in ASP.NET MVC. From their site:

Why Flush Early?

Flushing early can provide performance improvements in web applications and has been a recomended best practice in the web performance community since 2007.

To find out more, check out Nik's blog where he covered the benefits of flushing early in two posts:

It builds on top of ASP.NET ActionFilters, which you can apply as attributes to your methods, or call within controllers.

Let's say that you have some server-side work that's very slow. That slow operation could hold up the rendering of your page until it completes. With a pre-flush like this you can get bytes onto the network and into the user's browser faster.

Here we render some information and get it out fast before we do something that's unavoidably slow.

public ActionResult About()
{
ViewBag.Title = DateTime.Now.Second;
this.FlushHead();

Thread.Sleep(2000);
ViewBag.Message = "Your application description page.";

return View();
}

Let's think about really specifically. It's important to know WHY you would want to do this and what exactly happens in the browser.

If you have a long running, but important process (we are pretending that Thread.Sleep(2000) is important) that takes 2 seconds, no HTML is sent to the browser. It's just waiting. The timeline looks like this:

Here we didn't flush

See that blue line? We waited almost 5 seconds for the /about page, and while we were waiting, no Javascript and no CSS were being loaded. Why not? How could the browser know if it isn't seen the <head> of your HTML?

For an optimization, we could FLUSH the buffers that we have up to this point, putting the HTML that we have so far onto the network.

The Layout.cshtml we have a call to @Html.FlushHead() to get the the _Head.cshtml out and into the hands of the browser. It might look like this:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@if (ViewBag.Description != null)
{
<meta name="description" content="@ViewBag.Description">
}
<title>@ViewBag.Title - My ASP.NET Application</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>

Here's what the SAME page looks like with the <head> flushed out first.

Here we flushed

Look closely at the timeline. Here, I'll do it for you...below shows when we flushed early versus just waiting.

See how when we flushed the head it gave the browser enough information to stat loading some css and javascript we had in the <head?> The whole page took 5 seconds, but when we flush at least we get things going early, while when we don't flush we can't do a thing until the long running task finishes.

See the difference?

See the difference? Now, to be clear, don't blindly go and add optimizations without reading the code and understanding what's going on, but this is a nice tool for our ASP.NET toolbox.

We may see a similar but even more powerful technique in ASP.NET vNext that includes async flushes at multiple points, while updating the model as data is available.


Sponsor: Big thanks to Aspose for sponsoring the feed this week! Working with Files? Aspose.Total for .NET has all the APIs you need to create, manipulate and convert Microsoft Office documents and many other formats in your applications. Start a free trial today.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web

How to run Background Tasks in ASP.NET

August 26, '14 Comments [41] Posted in ASP.NET | NuGet | NuGetPOW | Open Source
Sponsored By

A few years back Phil Haack wrote a great article on the dangers of recurring background tasks in ASP.NET. In it he points out a few gotchas that are SO common when folks try to do work in the background. Read it, but here's a summary from his post.

  • An unhandled exception in a thread not associated with a request will take down the process.
  • If you run your site in a Web Farm, you could end up with multiple instances of your app that all attempt to run the same task at the same time.
  • The AppDomain your site runs in can go down for a number of reasons and take down your background task with it.

If you think you can just write a background task yourself, it's likely you'll get it wrong. I'm not impugning your skills, I'm just saying it's subtle. Plus, why should you have to?

There's LOT of great ways for you to do things in the background and a lot of libraries and choices available.

Some ASP.NET apps will be hosted in IIS in your data center and others will be hosted in the Azure cloud. The spectrum of usage is roughly this, in my opinion:

  • General: Hangfire (or similar similar open source libraries)
    • used for writing background tasks in your ASP.NET website
  • Cloud: Azure WebJobs 
    • A formal Azure feature used for offloading running of background tasks outside of your Website and scale the workload
  • Advanced: Azure Worker Role in a Cloud Service
    • scale the background processing workload independently of your Website and you need control over the machine

There's lots of great articles and videos on how to use Azure WebJobs, and lots of documentation on how Worker Roles in scalable Azure Cloud Services work, but not a lot about how your hosted ASP.NET application and easily have a background service. Here's a few.

WebBackgrounder

As it says "WebBackgrounder is a proof-of-concept of a web-farm friendly background task manager meant to just work with a vanilla ASP.NET web application." Its code hasn't been touched in years, BUT the WebBackgrounder NuGet package has been downloaded almost a half-million times.

The goal of this project is to handle one task only, manage a recurring task on an interval in the background for a web app.

If your ASP.NET application just needs one background task to runs an a basic scheduled interval, than perhaps you just need the basics of WebBackgrounder.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace WebBackgrounder.DemoWeb
{
public class SampleJob : Job
{
public SampleJob(TimeSpan interval, TimeSpan timeout)
: base("Sample Job", interval, timeout)
{
}

public override Task Execute()
{
return new Task(() => Thread.Sleep(3000));
}
}
}

Built in: QueueBackgroundWorkItem - Added in .NET 4.5.2

Somewhat in response to the need for WebBackgrounder, .NET 4.5.2 added QueueBackgroundWorkItem as a new API. It's not just a "Task.Run," it tries to be more:

QBWI schedules a task which can run in the background, independent of any request. This differs from a normal ThreadPool work item in that ASP.NET automatically keeps track of how many work items registered through this API are currently running, and the ASP.NET runtime will try to delay AppDomain shutdown until these work items have finished executing.

It can try to delay an AppDomain for as long as 90 seconds in order to allow your task to complete. If you can't finish in 90 seconds, then you'll need a different (and more robust, meaning, out of process) technique.

The API is pretty straightforward, taking  Func<CancellationToken, Task>. Here's an example that kicks of a background work item from an MVC action:

public ActionResult SendEmail([Bind(Include = "Name,Email")] User user)
{
if (ModelState.IsValid)
{
HostingEnvironment.QueueBackgroundWorkItem(ct => SendMailAsync(user.Email));
return RedirectToAction("Index", "Home");
}

return View(user);
}

FluentScheduler

FluentScheduler is a more sophisticated and complex scheduler that features a (you guessed it) fluent interface. You have really explicit control over when your tasks run.

using FluentScheduler;

public class MyRegistry : Registry
{
public MyRegistry()
{
// Schedule an ITask to run at an interval
Schedule<MyTask>().ToRunNow().AndEvery(2).Seconds();

// Schedule a simple task to run at a specific time
Schedule(() => Console.WriteLine("Timed Task - Will run every day at 9:15pm: " + DateTime.Now)).ToRunEvery(1).Days().At(21, 15);

// Schedule a more complex action to run immediately and on an monthly interval
Schedule(() =>
{
Console.WriteLine("Complex Action Task Starts: " + DateTime.Now);
Thread.Sleep(1000);
Console.WriteLine("Complex Action Task Ends: " + DateTime.Now);
}).ToRunNow().AndEvery(1).Months().OnTheFirst(DayOfWeek.Monday).At(3, 0);
}
}

FluentScheduler also embraces IoC and can easily plug into your favorite Dependency Injection tool of choice by just implementing their ITaskFactory interface.

Quartz.NET

Quartz.NET is a .NET port of the popular Java job scheduling framework of the (almost) same name. It's very actively developed. Quartz has an IJob interface with just one method, Execute, to implement.

using Quartz;
using Quartz.Impl;
using System;

namespace ScheduledTaskExample.ScheduledTasks
{
public class JobScheduler
{
public static void Start()
{
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
scheduler.Start();

IJobDetail job = JobBuilder.Create<MyJob>().Build();

ITrigger trigger = TriggerBuilder.Create()
.WithDailyTimeIntervalSchedule
(s =>
s.WithIntervalInHours(24)
.OnEveryDay()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(0, 0))
)
.Build();

scheduler.ScheduleJob(job, trigger);
}
}
}

Then, inside your Application_Start, you call JobScheduler.Start(). There's a great getting started article on Quartz at Mikesdotnetting you should check out.

Hangfire

And last but definitely not least, the most polished (IMHO) of the group, Hangfire by @odinserj. It's a fantastic framework for background jobs in ASP.NET. It's even optionally backed by Redis, SQL Server, SQL Azure, MSMQ, or RabbitMQ for reliability.

The Hangfire documentation is amazing, really. Every open source project's document should be this polished. Heck, ASP.NET's documentation should be this good.

The best feature from Hangfire is its built in /hangfire dashboard that shows you all your scheduled, processing, succeeded and failed jobs. It's really a nice polished addition.

image

You can enqueue "fire and forget" jobs easily and they are backed by persistent queues:

BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget"));

You can delay them...

BackgroundJob.Schedule(() => Console.WriteLine("Delayed"), TimeSpan.FromDays(1));

Or great very sophisticated CRON style recurrent tasks:

RecurringJob.AddOrUpdate(() => Console.Write("Recurring"), Cron.Daily);

Hangfire is just a joy.

Check out the Hangfire Highlighter Tutorial for a sophisticated but easy to follow real-world example.

There's a rich ecosystem out there ready to help you with your background tasks. All these libraries are excellent, are open source, and are available as NuGet Packages.

Did I miss your favorite? Sound off in the comments!


Sponsor: Many thanks to my friends at Raygun for sponsoring the feed this week. I *love* Raygun and use it myself. It's amazing. Get notified of your software’s bugs as they happen! Raygun.io has error tracking solutions for every major programming language and platform - Start a free trial in under a minute!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web

NuGet Package of the Week: MarkdownLog makes log files much prettier

July 30, '14 Comments [4] Posted in NuGet | NuGetPOW
Sponsored By

While I'm not 100% gaga over Markdown as I know many of you are, I definitely appreciate it's usefulness and it's clarity. Some folks would say I shouldn't rest until every binary document on my hard drive has been converted to Markdown. I say, nay nay. That said, I totally dig MarkdownPad and you should use it every day. It's lovely.

If you don't want to install anything, check out http://dillinger.io and quickly edit some Markdown in the browser.

image

But, I digress.

Note: Be sure to check out all the NuGet packages of the week! There's more!

The general idea behind Markdown is that HTML is way too complex for 90% of what you need, so rather than <ul> and <li> and all that, why not just express bullets with asterisks? We can all get behind that. There are many other simple Markdown syntaxes that you can learn in a few minutes. Once you've created some Markdown, you can easily generate PDFs, HTML, Word Documents, whatever you like.

Recently I stumbled upon Stuart Wheelwright's article on Using Markdown for Effective Logging over at CodeProject. Here's the first image borrowed from his excellent article where he clearly shows why Markdown is simpler than HTML:

html-vs-md

Stuart has taken Markdown and created a wonderful little library called MarkdownLog. It's a brilliant little idea that one of us should have come up with first! Kudos to Stuart. ;)

[MarkdownLog] is designed to make it trivial to produce Markdown formatted text from an application's data structures. Using just one line of code, a collection of .NET objects can be output as a table or list. And, because the output is Markdown, it can later be converted to HTML for publishing, if needed.

Even better, MarkdownLog is a Portable Class Library (PCL) and can be used with any .NET platform, including iOS with Xamarin. In fact, it's an iPhone app written with Xamarin that compelled Stuart to write MarkdownLog.

Here's one of his first examples. This C# code:

var myStrings = new[] { "John", "Paul", "Ringo", "George" };
Console.Write(myStrings.ToMarkdownBulletedList());

Gives you this simple Markdown.

   * John
* Paul
* Ringo
* George

At this point, you are likely unimpressed. But wait! There's more! There's a whole series of nice extension methods that make it easy to create templates from objects of any shape. Here's another example of his:

var data = new[]
{
new{Name = "Meryl Streep", Nominations = 18, Awards=3},
new{Name = "Katharine Hepburn", Nominations = 12, Awards=4},
new{Name = "Jack Nicholson", Nominations = 12, Awards=3}
};

Console.Write(data.ToMarkdownTable());

This gives you a Markdown table, of course. This looks nothing like an HTML table, but remember, Markdown is source code that can be translated into other formats like HTML and PDF.

 Name              | Nominations | Awards
----------------- | -----------:| ------:
Meryl Streep | 18 | 3
Katharine Hepburn | 12 | 4
Jack Nicholson | 12 | 3

Be sure to explore the full CodeProject article and the home page for MarkdownLog. Think about how you could add this to your existing logging framework and create better logs for tests, builds, anything. Take a look at the HTML render of one of Stuarts's Test Suite Runs and tell me that you want immediately want to get to work updating your old .LOG files.

Related Links

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Page 1 of 8 in the NuGet category Next Page

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.