Slides and Code from Orlando Code Camp 2012

01 Apr

My friend, Patrick Roeper, and I presented "Taking Control of WCF" yesterday at Orlando Code Camp 2012.

For those who attended, thank you! We hope you got something useful out of it, and that the concepts will help you start fighting back.

To the guy who said "restart Visual Studio!" when we had difficulty with our first demo solution, you were right. I reopened that solution afterward and added those service references without a hitch.

If you have any feedback you'd like to leave on the presentation, I'll appreciate your rating and comments on SpeakerRate.

Without further ado, the slides and code:
Taking Control of WCF Slide Deck (1.2 MB)
Taking Control of WCF Demo Code (187 KB)

We've plugged in a bit of dummy data (AdventurePerksDataContext) to remove the dependency of having an AdventureWorks SQL database running, and have turned off the Audit feature. If you turn on Auditing (via Audit=true when applying the ServiceExtensionsAttribute), audit records will be written to c:\wcfaudit.txt.

As Patrick mentioned, this code was written for demonstration purposes, to show how you can understand and take control of the communication framework using design principles with which you are already familiar. It is neither intended nor recommended for a production environment. It would probably get you fired.
posted @ Sunday, April 01, 2012 12:11 PM | Feedback (0)

Readable bool

16 Mar

The advent of .NET 4.0 has meant good things for code readability by way of named parameters. For all but the least motivated among us, obscure things like:

var employee = new Employee("John Doe", 23, true, false);

can become

var employee = new Employee(
    name: "John Doe",
    age: 23,
    isActive: true,
    isExempt: false);

Now we know exactly what we are doing when we pass those cryptic values, especially the booleans.

I work on a project that interfaces with a handful of external APIs, each with slightly different capabilities. These external providers are finite in number, and any change to the supported providers or capabilities of those providers requires some work in one or more translation modules, so we are not concerned with having the capabilities of each provider data-driven. Given these conditions, we define capabilities in code, passing in a value for each API provider capability as we construct the Provider object.

Important to note is that this class became not only a way to programmatically inspect the capabilities of a given API provider, but a human-readable reference document for looking up the capabilities of each provider when questions arose.

At first, it looked something like this:

FooProvider = new ApiProvider("Foo", true, false);
BarProvider = new ApiProvider("Bar", false, false);
BazProvider = new ApiProvider("Baz", true, true);

We could easily see from the constructor for ApiProvider that the first boolean parameter corresponded to canCancel and the second to canUpdate.

As we implemented support for more API providers, however, the number of nuances increased and the capabilities list increased as well. Time for named parameters.

FooProvider = new ApiProvider("Foo", canCancel: true, canUpdate: false, canReplayFeed: false, canLock: false);
BarProvider = new ApiProvider("Bar", canCancel: false, canUpdate: true, canReplayFeed: false, canLock: true);
BazProvider = new ApiProvider("Baz", canCancel: true, canUpdate: true, canReplayFeed: true, canLock: false);
FunkProvider = new ApiProvider("Funk", canCancel: true, canUpdate: true, canReplayFeed: true, canLock: false);

The named parameters help, but things are starting to get out of hand. We could break each capability out into its own line, but then it becomes difficult to compare the capabilities of each API provider, because each is separated by a considerable number of lines of code.

To mitigate this, we started by declaring constant bool members--all true--for each capability.

private const bool CanCancel = true;
private const bool CanUpdate = true;
private const bool CanReplayFeed = true;
private const bool CanLock = true;

We then passed these values in to our constructor instead of using named parameters, negating the value where the capability is not supported:

FooProvider = new ApiProvider("Foo", CanCancel, !CanUpdate, !CanReplayFeed, !CanLock);
BarProvider = new ApiProvider("Bar", !CanCancel, CanUpdate, !CanReplayFeed, CanLock);
BazProvider = new ApiProvider("Baz", CanCancel, CanUpdate, CanReplayFeed, !CanLock);
FunkProvider = new ApiProvider("Funk", CanCancel, CanUpdate, CanReplayFeed, !CanLock);

The effect is somewhat cleaner, and gives a relatively clear view of the capabilities of each provider.

This can be further enhanced by putting meaningful comments on each of the "readable bool" constants to give some hints via Intellisense:

a tool-tip shows more information about the meaning of the capability setting

This is a technique to pass meaningfully named bool arguments with or without .NET 4.0, optionally with some additional, descriptive metadata.

As when passing unnamed boolean arguments, however, it is critical that the values be ordered properly: there is no constraint against passing CanReplayFeed as the canCancel argument, for example.


tags: ,
posted @ Friday, March 16, 2012 1:06 AM | Feedback (1)

Secretly Sequential if-Blocks

14 Feb

There is a class of algorithm that always just feels off in its implementation, which typically takes the shape of a sequence of if blocks. In some cases, each subsequent if statement has to re-examine what happened in the previous.

As an example, consider the requirement to display a DateTime or TimeSpan in the format "11h 3m 55s", where no hour is displayed when the number of hours is 0 ("15m 9s"), no minutes displayed when the number of minutes is 0 ("59s") and nothing displayed if there are no seconds, except that if any of the preceding values is non-zero, we display subsequent values even if zero ("2h 0m 0s").

Definitely manageable, but not as straightforward as we'd like:

public static string FormatDisplayTime(DateTime time)
{
  var stringBuilder = new StringBuilder();

  var hasHours = time.Hour > 0;
  if (hasHours)
  {
    stringBuilder.Append(time.Hour).Append("h ");
  }

  var hasMinutes = time.Minute > 0;
  if (hasHours || hasMinutes)
  {
    stringBuilder.Append(time.Minute).Append("m ");
  }

  if (hasHours || hasMinutes || time.Second > 0)
  {
    stringBuilder.Append(time.Second).Append("s");
  }

  return stringBuilder.ToString();
}

 

Secretly sequential

The interesting thing about this--and it isn't immediately obvious--is that we are secretly dealing with a sequence.

I use the term "secretly sequential" lightly. It is natural that any series of if-blocks is crafted as a sequence -- the boolean logic simply doesn't work otherwise. What I'm talking about here are cases where the subjects of those if-blocks can actually be re-framed as a sequence.

So, we have 3 items in our sequence (hours, minutes, seconds), and we want to skip items until we find one that is non-zero. From that point on, we will print all items regardless of value.

Wow - this sounds like a job for LINQ.

 

Enter LINQ

We will have to restructure the problem as a sequence, though:

public static string FormatDisplayTime(DateTime dateTime)
{
  var dateParts = new Dictionary<string, int>
    {
      {"h ", dateTime.Hour},
      {"m ", dateTime.Minute},
      {"s", dateTime.Second}
    };
  return dateParts
    .SkipWhile(x => x.Value == 0)
    .Aggregate("", (s, x) => s + x.Value + x.Key);

In a nutshell, we set up our values and just skip over them until we find one with a non-zero value. From then on, we concatenate the value and the "h", "m", or "s" until we have gone over all items in the sequence and have our fully constructed time format.

 

Concatenation? What about StringBuilder?

You can use StringBuilder with Aggregate; it just gets a bit long-winded for my taste:

return dateParts
  .SkipWhile(x => x.Value == 0)
  .Aggregate(new StringBuilder(),
    (sb, x) => sb.Append(x.Value).Append(x.Key),
    sb => sb.ToString());

 

What about performance?

As you might expect, the cost of instantiating the Dictionary<TKey, TValue> outweighs the multiple evaluations in the "normal" implementation. At reasonable volume, the difference is indiscernible to negligible, but at 1 million consecutive invocations the difference is pronounced.

This is a not-uncommon tradeoff when using LINQ: sacrifice a bit of performance in favour of brevity, clarity and maintainability.

 

In conclusion

Whether you feel the LINQ solution is appropriate, better, or utter nonsense, I think it is a valuable exercise to recognize that some algorithms are "secretly sequential" and may lend themselves to being re-framed and solved as sequences.


tags: , ,
posted @ Tuesday, February 14, 2012 5:19 PM | Feedback (2)

Iterator Blocks and Side Effects

13 Feb

Sometimes leveraging side-effects in methods or even properties seems like a clever way to keep our code concise, but this often results in bugs and almost always comes at the cost of maintainability.

Avoiding side effects is especially important in the context of iterator blocks.

Consider the following method:

public IEnumerable<Customer> GetOrLoad(IEnumerable<int> customerIds)
{
    foreach (var id in customerIds)
    {
        Customer found = null;
        if(!_innerCache.TryGetValue(id, out found))
        {
            found = PerformLookup(id);
            _innerCache.Add(id, found);
        }

        yield return found;
    }
}


To a caller, this method just returns IEnumerable<Customer>; there is no immediate indicator that the method is implemented as an iterator block (using the yield keyword). The signature of the method suggests that we are dealing with a cache of Customer objects, keyed on an int identifier, and that if we pass in some IDs they'll either be returned from the cache or else some kind lookup will be performed and the result will then be cached.

On the one hand, the naming is decent: "GetOrLoad" gives us a clue that we aren't just drawing from the cache -- that we might be loading some data. However, that same hint suggests that we could use the same method as a means of loading items into the cache.

So, we do some work when the application is initializing in order to prime the cache:

public void PreloadCustomerCacheForSalesPerson(SalesPerson salesPerson)
{
    _cache.FindOrLookup(salesPerson.CustomerIds);
}

We know that the GetOrLoad() method is going to look up any values not already in cache, and add them. We aren't concerned here with the Customer objects that the method will return.

The problem is that we've gone through all this work to prime the cache for our salespeople, but the cache is still completely empty.

We were relying on the knowledge that this method will look up Customers that aren't already cached, but because the method is implemented as an iterator (therefore lazy-evaluated) and we never iterate over the result set, the body of the GetOrLoad() method is never invoked.

tags: , ,
posted @ Monday, February 13, 2012 6:14 PM | Feedback (0)

GiveCamp Orlando 2011 Project Summaries

02 Nov

Now nearly two weeks gone, the first of what will hopefully be annual GiveCamp Orlando events is starting to feel a little distant and I'm starting to get a little nostalgic.

If you're not familiar with GiveCamp, you can learn more at http://givecamporlando.org.

As expected, I learned a lot about putting such an event together by doing it and making mistakes. I'll get to lessons learned in my next post, but I don't want to lose sight of the successes that our volunteers achieved on behalf of some wonderful Central Florida non-profits, so let's take a look at those first.

Rotary Club of Lake Nona

This relatively new Rotary Club had a domain name, but no site. Richard took the lead on putting together a new WordPress based site, with an assist from Mauricio, who came up from South Florida to help out on Saturday.

Rotary Club of Lake Nona

 

Habitat for Humanity of Greater Orlando

This organization has an aging site with some stale content, only small parts of which could be updated by in-house staff. Habitat sought to create a fresh look and consolidate their message and content. Emmanuel and Jon, grad students at the University of Central Florida, took the lead on designing and implementing a new WordPress site, which will be going live when one final component can be migrated over from the existing site.

Habitat for Humanity of Greater Orlando

 

Homegrown Local Food Cooperative

Like Habitat, the Homegrown Co-op had an aging site that was difficult to modify. Vish and Marcela put a lot of work into a new WordPress site with a very clean template, and Adam put a lot of work into creating fresh visual assets for the site. In addition, Brian worked on custom CSS to style Homegrown's 3rd party online farm store portal to match the look and feel of their new site. Homegrown's new site will be going live when their staff are able to finish gathering and adding photographs.

Homegrown Local Food Cooperative

 

Florida Literacy Coalition

Wayne spent some time optimizing the homepage for mobile browsing, but the bigger project for GiveCamp was implementing a mapping feature for FLC's county-by-county directory of literacy service providers and volunteer opportunities. The number of data points and the integration story with the existing site made this an unexpectedly challenging project, but it came together through the diligent effort of Dave and Jack, with a little help from Eddy and Jay. This feature is in production at http://floridaliteracy.org.

Florida Literacy Coalition mapping feature

 

Business Advisory Council of the Center for Independent Living

The Council is a project of CIL focused on educating business about the disability workforce and fostering connections between employers and job-seekers. BAC did not have a website, but thanks to the efforts of Kathy and Grant they now have a great-looking site in production at http://baccf.org. In addition, Michael updated their logo.

Business Advisory Council - Center for Independent Living

 

Down Syndrome Foundation of Florida

Breaking stride from our many WordPress implementations, Carlos, Ketema, Kathy, and Brandon helped the Down Syndrome Foundation of Florida migrate their existing content to a new Squarespace site.

The Foundation has a lot of forms on their site, some of which were implemented using the Squarespace form builder, which allows data to be exported to Excel.

For the other forms, Michael and Hugo are putting togther a custom ASP .NET form builder. The new website will go live at http://dsfflorida.org when all content has been migrated to the new platform.

 

The Getaboard Foundation

This project turned out to be most challenging, because the Foundation already has a nicely designed site at http://getaboard.org. Like many others, though, this was custom development and the site could not be updated. Brian worked on swapping out the Flash image slider in favor of a jQuery slider. It turned out that we had PHP talent in Marcela, who integrated a Tumblr feed and quickly built a pretty sophisticated editor interface that allows the Getaboard guys to update their events and other non-static content.

Post GiveCamp, Leon is working on the last pieces of the puzzle to freshen up the site and provide the desired level of editability.

 

Faith Arts Village of Orlando

The FAVO site needed a makeover. Hannah and Z created a clean, new WordPress site, and Brian worked on possible logo redesigns. This site will be going live when the DNS keys can be found and turned.

 

Compassion Corner Ministry

This organization provides assistance and counseling to the homeless population of downtown Orlando, and sought to improve the way they track the individuals they serve and the history of services provided. Patrick and Mark spent some time gathering requirements and then built an ASP .NET MVC application to provide the necessary functionality. Eddy, Esteban and Z collaborated on day 3 to bring the project to completion. The result is a fully custom solution that will enable this organization to vastly improve the efficiency and accuracy of tracking and reporting their efforts.

 

Adult Literacy League

Seeking social media guidance, we set the League up with some faculty volunteers from Full Sail University's Internet Marketing Department. They met the Tuesday following GiveCamp for a consultation and some implementation work.

 

Conrad Educational Foundation

This community foundation needed a website, and got one thanks to the efforts of Tampa Bay GiveCamp. Tampa had more volunteers than projects and we had more projects than volunteers, so it worked out nicely. The volunteers in Tampa put together the new http://conradeducationfoundation.org using DotNetNuke.


Did you work on one of these projects?

I tried to recognize the core group of volunteers that worked on each of these projects, but if I missed you I'd like to make it right. Just send me a note.
posted @ Wednesday, November 02, 2011 11:13 PM | Feedback (0)

Another Approach to Checking for Null or Empty in C#

04 Sep

 

The joy of extension methods

Checking whether a string or collection is null or empty is something we have to do relatively frequently. The advent of extension methods gave us a pretty clean way to wrap these tasks up with a nice, readable, left-to-right syntax:

IEnumerable myCollection;
if (myCollection.IsNullOrEmpty())…

 

Negative assertions

I've had a long-standing ambivalence, however, about negating these methods. The options either forfeit the left-to-right syntax or are difficult to discern from the positive assertion:

if (!myCollection.IsNullOrEmpty())…
if (myCollection.IsNotNullOrEmpty())…

Sometimes the call to IsNullOrEmpty follows a method call that returns a string, perhaps with parameters, such that the ! is all but forgotten by the time it matters, while paired methods IsNullOrEmpty and IsNotNullOrEmpty become such a jumble of letters when used close to eachother in a block of code that it becomes easy to miss the negation.

 

A new alternative: Null.OrEmpty

I'm offering another alternative here that gives up some of the plain language in exchange for consistency with typical null-checks.

if (myCollection == Null.OrEmpty)…
if (myCollection != Null.OrEmpty)…

if (myString == Null.OrEmpty)…
if (myString != Null.OrEmpty)…

While I am a big fan of plain language for readability, I think using == and != operators
  • is clear
  • can utilize whitespace to consistently draw the eye to the nature of the assertion (positive or negative)
  • maintains consistency with checks for null or string.Empty
  • preserves left-to-right reading
All of these conditions and their inverses are supported:
if (null == Null.OrEmpty)… // true
if ("" == Null.OrEmpty)… // true
if (new List<int>() == Null.OrEmpty)… // true

 

The code

Here is the class that makes it possible:
using System.Collections;
using System.Linq;

namespace System
{
  public class Null
  {
    private static readonly Null Instance = new Null();

    private Null() { }

    public static Null OrEmpty
    {
      get { return Instance; }
    }

    public static bool operator ==(IEnumerable collection, Null n)
    {
      return collection == null || !collection.Cast<object>().Any();
    }

    public static bool operator !=(IEnumerable collection, Null n)
    {
      return !(collection == n);
    }

    public static bool operator ==(Null n, IEnumerable collection)
    {
      return collection == null || !collection.Cast<object>().Any();
    }

    public static bool operator !=(Null n, IEnumerable collection)
    {
      return !(collection == n);
    }

    public static bool operator ==(string s, Null n)
    {
      return string.IsNullOrEmpty(s);
    }

    public static bool operator !=(string s, Null n)
    {
      return !(s == n);
    }

    public static bool operator ==(Null n, string s)
    {
      return string.IsNullOrEmpty(s);
    }

    public static bool operator !=(Null n, string s)
    {
      return !(s == n);
    }

    public override bool Equals(object obj)
    {
      if (obj == null) return true;

      var asNull = obj as Null;
      if (asNull != null) return true;

      var asString = obj as string;
      if (asString != null) return asString == Instance;

      var asEnumerable = obj as IEnumerable;
      if (asEnumerable != null) return asEnumerable == Instance;

      return false;
    }

    public override int GetHashCode()
    {
      return 0;
    }
  }
}

This demonstrates the basic pattern, and can easily be extended to support other types of checks, such as IsNullOrWhitespace.


tags: ,
posted @ Sunday, September 04, 2011 1:35 AM | Feedback (0)

Slide Deck: C# API Design

16 Jun

Here are the slides from tonight's C# API Design presentation at ONETUG.

I know that I didn't have much time to talk about specific design challenges or take questions; please feel free to contact me with any questions, and don't forget to rate and/or comment on the presentation at SpeakerRate.

c_sharp_api_design.zip

posted @ Thursday, June 16, 2011 8:15 PM | Feedback (0)

Announcing GiveCamp.Orlando

06 May

GiveCamp.Orlando will be an opportunity for developers, designers, database experts, business analysts, and other technical professionals to volunteer their time and expertise for the benefit of local non-profit organizations.

GiveCamp.Orlando logo

GiveCamp.Orlando will coincide with GiveCamps in other cities October 21 - 23, 2011 as part of National GiveCamp.

You can learn more about GiveCamp at givecamp.org, and learn more about and/or register your interest in the local event at givecamporlando.org.

Also on twitter: @givecamporlando (use hashtag #givecamporl) and facebook

Please actively promote this event among friends, coworkers and the like. We need tech professionals, but we also need non-profits and plenty of volunteers in non-technical roles.

Update:
We are excited to announce that GiveCamp.Orlando will be held at Full Sail University!
posted @ Friday, May 06, 2011 9:49 AM | Feedback (0)

FluentWorker

21 Apr

This is a little library that I put together to provide a fluent API over the .NET BackgroundWorker class.

FluentWorker sample usage
  1. FluentWorker
  2.     .Do(SomeVoidMethod)
  3.     .WhenDone(() => Console.WriteLine("Done!"))
  4.     .Start();

 

FluentWorker addresses what I see as the burdensome repetition of employing BackgroundWorker, which typically involves:

  1. instantiation
  2. several lines of settings
  3. multiple event subscriptions
  4. creating handlers for all of the events
  5. starting the BackgroundWorker
  6. disposing of the BackgroundWorker
As point of reference, the following is roughly the equivalent of the 4 lines shown above:
Straight-up BackgroundWorker
  1.     // …
  2.     var worker = new BackgroundWorker();
  3.     worker.DoWork += worker_DoWork;
  4.     worker.RunWorkerCompleted
  5.         += worker_RunWorkerCompleted;
  6.     worker.RunWorkerAsync();
  7. }
  8.  
  9. static void worker_DoWork
  10.     (object sender, DoWorkEventArgs e)
  11. {
  12.     SomeVoidMethod();
  13.     ((BackgroundWorker) sender).DoWork
  14.         -= worker_DoWork;
  15. }
  16.  
  17. static void worker_RunWorkerCompleted(object sender,
  18.     RunWorkerCompletedEventArgs e)
  19. {
  20.     Console.WriteLine("Done!");
  21.     var worker = (BackgroundWorker) sender;
  22.     worker.RunWorkerCompleted
  23.         -= worker_RunWorkerCompleted;
  24.     worker.Dispose();
  25. }

…and this is (1) for the simplest case, and (2) required for every usage of BackgroundWorker. Adding progress notifications and cancellation support require more lines of configuration and yet another event handler delegate.

FluentWorker can reduce virtually all of this to a single expression in the consumer. While you may still use multiple lines, the code is terse and declarative.

FluentWorker handles event subscription and unsubscription internally. Rather than creating handler methods in your classes, you supply simple void delegates, which can be in-line/anonymous Actions or member methods.

An arbitrary number of actions can be added to the worker's task list by calling Do() repeatedly, and an arbitrary number of actions can executed upon completion of those tasks by calling WhenDone() repeatedly. Tasks will be executed synchronously on the background thread. This can be used to partition and "queue" tasks, and to keep progress reporting logic out of the processing logic.

Adding multiple Do() and WhenDone() actions
  1. FluentWorker
  2.     .Do(FirstTaskMethod)
  3.     .Do(t => t.ReportProgress(25))
  4.     .Do(SecondTaskMethod)
  5.     .Do(t => t.ReportProgress(75))
  6.     .Do(() => Console.WriteLine("ThirdMethodInline"))
  7.     .Do(t => t.ReportProgress(100))
  8.     .WhenDone(DataLoaded)
  9.     .WhenDone(() => Console.WriteLine("Done!"))…


Subsequent calls to Start() will repeat the worker's set of actions. If a worker is running when Start() is called, another run will be queued for that worker and the WhenDone() actions will execute only once. If a worker is not running, it will spin up a new BackgroundWorker internally and WhenDone() actions will be executed when it completes its work.

FluentWorker can be configured to handle exceptions with a custom Action<Exception> and Continue(), Cancel() or Throw() the Exception.

Exception action and continuation strategy
  1. FluentWorker
  2.     .Do(SomeVoidMethod)
  3.     .OnException(x => x.Do(
  4.        ex => Log.Error(ex, "Error in bg process")).Continue())…


A thread-safe progress reporting strategy can be defined by supplying an Action<int>.

Setting the progress-reporting strategy
  1. FluentWorker
  2.     .Do(SomeVoidMethod)
  3.     .ReportsProgressBy(i => progressBar.Value = i)…

Progress reporting and cancellation-checking is simplified in Do() task delegates by using the Do(ITaskHelper) overload.

Using ITaskHelper to check for cancellation
and invoke the progress-reporting strategy
  1. FluentWorker.Do(t =>
  2.     {
  3.         t.ReportProgress(0);
  4.         // do something
  5.         t.ReportProgress(50);
  6.         if(t.Cancelled) return;
  7.         // do something else
  8.         if(t.Cancelled) return;
  9.         t.ReportProgress(100);
  10.     })

Calling ReportProgress(i) will execute your configured ReportsProgressBy() Action<int>.

Calling Restart() on a FluentWorker instance is like calling Cancel() and Start() in succession, except that Restart() will always kick off a new BackgroundWorker immediately, even if active BackgroundWorkers are still winding down. A BackgroundWorker that is canceled as a result of a call to Restart() will not execute its WhenDone() actions; they will be executed when the new worker completes processing.

FluentWorker disposes of its BackgroundWorker automatically when processing is complete. As mentioned previously, a subsequent call to Start() will spin up a new BackgroundWorker() if none is active. Lifecycle management of the component is trasparent to the consumer.

 

Disclaimer

There is a lot of room here for expansion and--no doubt--improvement. I'm no expert in multithreading, and haven't put this code through any serious paces, so of course the supplied code comes as-is, without warranty, express or implied.

 

Summary

There are myriad ways to stow BackgroundWorker's heavy baggage safely and securely. FluentWorker is my first pass at encapsulating the complexity and re-exposing BackgroundWorker's powerful functionality in a simple, reusable component with a clean, fluent interface.

 

Downloads

I gave a brief presentation on this at tonight's ONETUG meeting. The slides are from that presentation.
FluentWorker_v20110421.zip | FluentWorkerSlides.zip
posted @ Thursday, April 21, 2011 9:09 PM | Feedback (0)

How to solve memory problems in your WPF application

15 Apr

Wow, look what I found.

.NET developers, you get one guess what this bad-boy menu item does.

Application Menu Item: "GC"


When you reach the point of deciding to add this button, it is safe to assume that you've gone hopelessly wrong somewhere.

To be fair, it did bring this application's Task-Manager-reported memory usage down… from 500MB to 250MB.
posted @ Friday, April 15, 2011 11:34 AM | Feedback (0)