Hugoware

The product of a web developer with a little too much caffeine

Posts Tagged ‘Source Code

More Control When Parsing Values

with 4 comments

As much as you might try, sooner or later you’re going to wind up parsing string values and converting them into their correct type. It is… inevitable.

If you’re going for clarity then this only takes a couple lines of code to accomplish. For example…

int value = default(int);
int.TryParse("44", out value);

No big deal, in fact it is possible to do on a single line of code…

int value = int.TryParse("44", out value) ? value : default(int);

Of course, this line of code is going to draw more questions from some of the less experienced developers on your team, but hey… doesn’t it just scream ‘Alpha Programmer’?

Parsing integers isn’t really a big deal in itself but wouldn’t it be nice if the entire process could be done on one line with one method and allow you to supply default values… and without causing confusion?

Below is an example of how you might create a class that handles parsing for you.

/// <summary>
/// Handles parsing string values to appropriate types
/// </summary>
public static class Parse {

	#region Static Constructor

	//prepares the Parse class
	static Parse() {
		Parse._CustomParsing = new Dictionary<Type, Func<string, object>>();
	}

	#endregion

	#region Properties

	//holds custom parse methods
	private static Dictionary<Type, Func<string, object>> _CustomParsing;

	#endregion

	#region Public Methods

	/// <summary>
	/// Registers a custom parsing method
	/// </summary>
	public static void RegisterType<T>(Func<string, object> compare) {
		Type type = typeof(T);
		Parse._CustomParsing.Remove(type);
		Parse._CustomParsing.Add(type, compare);
	}

	/// <summary>
	/// Attempts to parse a value and return the result
	/// but falls back to the default value if the conversion fails
	/// </summary>
	public static T TryParse<T>(string value) {
		return Parse.TryParse(value, default(T));
	}

	/// <summary>
	/// Attempts to parse a value and falls back to the
	/// provided default value if the conversion fails
	/// </summary>
	public static T TryParse<T>(string value, T @default) {
		value = (value ?? string.Empty).ToString();
		Type type = typeof(T);

		//so much can go wrong here, just default to the
		//fall back type if the conversions go bad
		try {

			//perform custom parsing first
			if (Parse._PerformCustomParse(type, value, ref @default)) return @default;

			//check if this is a nullable and if we should use a child type
			//this might not work with VB since the nullable name could be different
			if (type.IsGenericType && type.Name.StartsWith("Nullable`")) {
				
				//underlying type for a nullable appears to be the first argument
				type = type.GetGenericArguments().FirstOrDefault();
				
				//if no type was found then five up
				if (type == null) { return @default; }

				//try custom parsing with the underlying type if this was a nullable
				if (Parse._PerformCustomParse(type, value, ref @default)) return @default;
			}

			//try the remaining parsing methods
			if (type.IsEnum && Parse._PerformEnumParse(type, value, ref @default)) return @default;
			if (Parse._PerformParse(type, value, ref @default)) return @default;

			//finally, just try a conversion
			Parse._PerformConvert(type, value, ref @default);
			return @default;

		}
		//settle for the default
		catch {
			return @default;
		}

        }

	#endregion

	#region Checking Values

	//uses custom parsing methods
	private static bool _PerformCustomParse<T>(Type with, string value, ref T result) {

		//if there is no custom type, cancel
		if (!Parse._CustomParsing.ContainsKey(with)) { return false; }

		//find the conversion
		Func<string, object> parse = Parse._CustomParsing[with];

		//attempt to parse
		try {
			object converted = parse(value);
			bool success = converted is T;
			if (success) { result = (T)converted; }
			return success;
		}
		//if the attempt failed
		catch {
			return false;
		}

	}

	//tries to parse using an Enum
	private static bool _PerformEnumParse<T>(Type with, string value, ref T result) {

		//check for a result
		try {
			object parsed = Enum.Parse(with, value, true);
			bool success = parsed is T;
			if (success) { result = (T)parsed; }
			return success;
		}
		catch {
			return false;
		}

	}

	//searches for a 'Parse' method
	private static bool _PerformParse<T>(Type with, string value, ref T result) {

		//make sure a try parse was even found
		MethodInfo method = with.GetMethods().FirstOrDefault(item =>
			item.Name.Equals("parse", StringComparison.OrdinalIgnoreCase) &&
			item.IsStatic);
		if (method == null) { return false; }

		//check for a result
		try {
			object parsed = method.Invoke(null, new object[] { value, result });
			bool success = parsed is T;
			if (success) { result = (T)parsed; }
			return success;
		}
		catch {
			return false;
		}

	}

	//performs common conversions
	private static bool _PerformConvert<T>(Type type, string value, ref T result) {
		object convert = Convert.ChangeType(value, type);
		bool success = convert is T;

		//update the type if needed
		if (success) { result = (T)convert; }
		return success;
	}

	#endregion

}

The general idea behind this code is to attempt to parse using a variety of ways and then fall back to a default value if nothing works out. This way we can get in and out of parsing values without needing to split up the code.

There is an ugly little bit of reflection in there, so if your application can’t incur that kind of penalty hit you might want to fall back to the classic parsing methods.

int count = Parse.TryParse("44", 0); // 44
bool success = Parse.TryParse("cheesecake", true); // true
decimal = Parse.TryParse("3.01", 3M); // 3.01

Additionally, this code allows you to add your own parsing methods to the class and use the same method across the entire project.

//register our own parsing method
Parse.RegisterType<Color>(value => ColorTranslator.FromHtml(value));
Parse.RegisterType<decimal>(value => {
    value = Regex.Replace(value, @"[^0-9\.]", string.Empty);
    return decimal.Parse(value);
    });

//use it just like any other value
Color color = Parse.TryParse("#ff0000", Color.Blue);
decimal total = Parse.TryParse("$45.33", 0M);

You could take the extension methods a step further by adding the same functionality to your strings. For example.

/// <summary>
/// Extensions to quickly parse string types
/// </summary>
public static class StringExtensions {

	/// <summary>
	/// Parses a string to the correct type or default value
	/// </summary>
	public static T ToType<T>(this string value) {
		return value.ToType(default(T));
	}

	/// <summary>
	/// Parses a string to the correct type or default value
	/// </summary>
	public static T ToType<T>(this string value, T @default) {
		return Parse.TryParse(value, @default);
	}
	
}

This would allow you to perform the conversions from the string itself and avoid the call to a static method floating around in the project.

bool admin = "true".ToType<bool>();
int count = "3334".ToType<int>();
Color blue = "something".ToType(Color.Blue);
Color green = "#00ff00".ToType(Color.Red);

Parsing values isn’t hard but is certainly doesn’t hurt to make it a bit easier.

Advertisements

Written by hugoware

August 5, 2010 at 1:29 am

Using WebControls In ASP.NET MVC Views – Part 3

with 18 comments

Check out my newest blog post about using WebControls inside of MVC (source code included)🙂

WebControls In MVC Series

I mean’t to have this post out this morning, but I was busy working on getting my personal website online — Please check it out and tell me what you think!

Up until now all of the post in this series have been tests to see if we could make the process work. Now, finally, now we get into some real code that we can actually use. I’ll take just a little bit of space to explain how it works and how to set it up, but then the rest of the post to explain how to use it.

Getting The Code Setup

Before we discuss how to use the code lets go over briefly how to setup the code.

To start, the code you need to download is found at the end of the post. When you download it, put both the .cs and the .aspx file into your project. By default the code expects the .aspx to be in your Views directory, but you can move it — but if you do then you need to update MVCWebFormExtensionMethods._WebFormRenderControl.TEMPLATE_PAGE with the new location (yeah, it’s a long name :)).

Optionally, you can make a couple changes to your Web.config to make sure that the extension method and MvcControlGroup are available to the rest of your application without needing to add anything special onto each of your pages.

<configuration>
    <!-- snip... -->
    <system.web>
        <pages>
            <controls>
                <!-- snip... -->
                <add tagPrefix="mvc" 
					namespace="MvcWebControls" 
					assembly="YourProjectAssemblyName" />
            </controls>
            <namespaces>
                <!-- snip... -->
                <add namespace="MvcWebControls"/>
            </namespaces>
        </pages>
    </system.web>
    <!-- snip... -->
</configuration>

I’ve hidden most of the Web.config from this example, so make sure to add the areas you see above, not simply replace your entire config.

The rest of this post goes over some demos found out on my website, you may want to follow along to better understand what is going on.

Example 1 — Simple Postbacks

<html>
    <head runat="server">
        <title>Simple Postback Example</title>
    </head>
    <body>    
        <h2>Just A Simple Form</h2>
        <% this.Html.WebForm((form) => { %>        
            <% form.RenderControl(new TextBox()); %>        
            <% form.RenderControl(new Button() { Text = "Save" }); %>
            <hr />
            <% form.RenderControl(new Calendar()); %>
        <% }); %>    
    </body>
</html>

For the first example, we want to see if we can use a few simple WebControls and see if our values are posted back the way we would expect. In this example we add a TextBox, Button and Calendar. Pay attention to how this HtmlHelper method works.

If you notice, the method excepts a delegate to render the control onto the page. We do it like this because it allows us to provide markup to our page along side with the WebControls. The method accepts a single parameter the MvcWebForm. Think of this as a very, very simple version of a WebForm.

The MvcWebForm gives you access to the page that is being used to render the controls. This is important to remember and I’ll go over it in the next section.

Example 2 — Using Events

This is handy so far but unless we are using some events for our controls, nothing much has changed. Let’s look at another example.

<html>
    <head runat="server">
        <title>Using Events</title>
    </head>
    <body>    
        <h2>Simple Button Click Event</h2>
        <% this.Html.WebForm((form) => { %>        
            <strong>Clicked : </strong>
            <% Label text = form.RenderControl(new Label() { Text = "0" }); %>
            
            <br /><br />            
            <% Button submit = form.RenderControl(new Button() { Text = "Add" });
               submit.Click += (s, e) => {
                   int value = 0;
                   int.TryParse(text.Text, out value);
                   text.Text = (++value).ToString();
                   
                   //post back this information
                   form.Page.ClientScript.RegisterStartupScript(
                       typeof(Page),
                       "confirm",
                       string.Format("alert('Updated to {0}');", text.Text),
                       true
                       );
               }; %>
        <% }); %>    
    </body>
</html>

In this example we are assigning a Click event to the button we added to the page. You’ll notice when we run this example the value is incremented up by one on each press.

Notice that we use form.Page instead of this.Page. When you use this code you must remember that you are working with two separate Page instances. One for the View and the other for rendering the control. In this example we use RegisterStartupScript to display an alert box. If you were to have used this.Page.ClientScript, nothing would happen.

It’s a subtle difference that you are going to want to keep in mind while you use this class.

Example 3 — MvcControlGroup

So far all of our examples have only used a couple controls that were all rendered inline with the rest of the page. While this is going to work in most situations, some places won’t work as well.

The example below shows how you can use the MvcControlGroup to group controls together, for example, the Wizard control.

<html>
    <head runat="server">
        <title>Using Events</title>
    </head>
    <body>    
        <h2>Using Wizard Control with MvcControlGroup</h2>
        <div>
        <% this.Html.WebForm((form) => { %>
        <mvc:MvcControlGroup runat="server" >
        <asp:Wizard runat="server" >
            <WizardSteps>
                <asp:WizardStep runat="server" Title="Personal Information" >
                    <p>Leaving fields blank will catch the validators.</p>
                    <strong>Name</strong>
                    <asp:TextBox ID="name" runat="server" />
                    <asp:RequiredFieldValidator runat="server" 
                        ControlToValidate="name" ErrorMessage="Must provide a name" />
                    <br /><br />
                
                    <strong>City</strong>
                    <asp:TextBox ID="city" runat="server" />
                    <asp:RequiredFieldValidator runat="server" 
                        ControlToValidate="city" ErrorMessage="Must provide a city" />                    
                </asp:WizardStep>
                
                <asp:WizardStep runat="server" Title="Reservation Date" >
                    <strong>Date Requested</strong>
                    <asp:Calendar ID="date" runat="server" />                 
                
                </asp:WizardStep>
                
                <asp:WizardStep runat="server" Title="Confirm" >
                    
                    <h2>Confirm This Order?</h2>
                    <p>This step maps to a Controller Action before submitting</p>
                    
                    <% MvcWebForm.Current.Action = "/Projects/MvcWebForms/Submit"; %>
                    <% MvcWebForm.Map(() => new {
                        name = name.Text,
                        city = city.Text,
                        date = date.SelectedDate.ToShortDateString()
                       }); %>
                       
                </asp:WizardStep>
            </WizardSteps>
        </asp:Wizard>
        </mvc:MvcControlGroup>
        <% }); %>
        </div>
        
    </body>
</html>

Just like with other controls, the MvcControlGroup must be rendered in a separate page instance. This control moves it to the correct context before it is processed.

If you remember, the idea behind this project was to allow people to use WebControls with Mvc — not just use WebControls in Mvc. On the last step we point our form to a Controller Action and use a method called Map to format our information for the postback.

When the user finished the Wizard they can post the finished form back to the correct Controller Action!

**whew** — that’s a lot to take in!

Limitations

There are a couple limitations that still need to be worked through…

  • HTML output must be well formed – If your control outputs bad HTML, this thing will croak. The reason is that instead of using Regular Expressions to try and match content the output is parsed using the XDocument.
  • PageMethods, AJAX calls will probably not work – Controls like the UpdatePanel or using PageMethods will most likely not work anymore. Some additional work is going to be required to make those work.

I’m sure there is more that needs to be looked at but at this point it seems as if it can be used — at least for testing.

Please let me know if you have any ideas, feedback or suggestions! I’m going to continue to work on this and see what kinds of improvements can be made.

Source Code

Download MvcWebForms.zip

.

Written by hugoware

August 12, 2009 at 8:58 pm