How to: Create a sketchy Siverlight GroupBox in Blend/SketchFlow

by ondrejsv 17. January 2010 18:17

Silverlight does not come with a groupbox control in its standard set of controls. Fortunately, it’s not so difficult to create a new one or you may grab Tim Greenfield’s Silverlight GroupBox control.

Having Tim’s control referenced, open the SketchStyles.xaml file in your screen mocks project, switch to the XAML view and at the end of the file before the final </ResourceDictionary> tag append this new style:

<Style x:Key="GroupBox-Sketch" TargetType="Groupbox:GroupBox">
<Setter Property="Background" Value="{StaticResource BaseBackground-Sketch}"/>
<Setter Property="Foreground" Value="{StaticResource BaseForeground-Sketch}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource BaseBorder-Sketch}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Groupbox:GroupBox">
<Grid Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<SketchControls:SketchRectangleSL Grid.Row="1" Grid.RowSpan="2" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<SketchControls:SketchRectangleSL.Clip>
<GeometryGroup FillRule="EvenOdd">
<RectangleGeometry x:Name="FullRect" Rect="0,0,300,200"/>
<RectangleGeometry x:Name="HeaderRect" Rect="6,0,100,100"/>
</GeometryGroup>
</SketchControls:SketchRectangleSL.Clip>
</SketchControls:SketchRectangleSL>
<ContentPresenter Margin="{TemplateBinding Padding}" Grid.Row="2" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ContentControl x:Name="HeaderContainer" HorizontalAlignment="Left" Margin="6,0,0,0" Grid.Row="0" Grid.RowSpan="2">
<ContentPresenter Margin="3,0,3,0" Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
</ContentControl>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="FontFamily" Value="{StaticResource FontFamily-Sketch}"/>
<Setter Property="FontSize" Value="{StaticResource SizeDouble-Sketch}"/>
<Setter Property="FontWeight" Value="{StaticResource FontWeight-Sketch}"/>
</Style>

At the beginning of the file add this new namespace declaration:

xmlns:Groupbox="clr-namespace:Groupbox;assembly=Groupbox"

Done! You may now create a sketchy groupbox:

image

image

kick it on DotNetKicks.com

Tags: ,

How to: Create a new format for sample data in Blend/SketchFlow

by ondrejsv 17. January 2010 14:04

Microsoft Expression SketchFlow (part of the Expression Blend Product) is a great tool for quickly creating interactive low-fidelity prototypes. One of its features is sample data. You can either import data from an XML file, type in it manually within the SketchFlow user interface or let it populate automatically from the Blend sample data collection. The last option is very effective if you are experimenting with many screen mocks.

Note: You may use sample data feature equally well in the SketchFlow as well as in the Blend itself.

Blend supports four data types in sample data: string, number, boolean and image. If you create a new string property you can tell Blend what format you want to have the property in – e.g. name, url address, e-mail or color. Blend then create a sample data based on this (in our case it may be “Jamison, Jay”, http://www.graphicdesigninstitute.com/, someone-3@adventure-works.com, #FFFFED6F).

You can also create a new format if you often create screen mocks with some specific data. For example let’s say that I often mock administration module prototypes and I would find useful to have a login name format to let Blend create sample data for me.

All sample data is stored in the C:\Program Files\Microsoft Expression\Blend 3\SampleDataResources\en\Data\SampleStrings.csv.

Tip: If you are running Blend in other language than English, you may create a subfolder with your language code (instead of the en) and it takes precedence over the en folder.

This file is nothing more that a comma separated data file. I can add a new column to the header and then new columns do data rows (new columns are last):

Name,Phone Number,Email Address,Website URL,Address,Company Name,Date,Time, Price, Colors,Login
"Aaberg, Jesper",(111) 555-0100,someone@example.com,http://www.adatum.com/,"4567 Main St., Buffalo, NY 98052",A. Datum Corporation,"November 5, 2003",8:20:14 AM,$100,#FF8DD3C7,jesper.aaberg
"Adams, Ellen",(222) 555-0101,user@adventure-works.com,http://www.adventure-works.com/,"1234 Main St., Buffalo, NY 98052",Adventure Works,"December 29, 2006",7:06:05 AM,$29.99,#FFFFFFB3,ellen.adams
"Adams, Terry",(333) 555-0102,someone@adventure-works.com,http://www.adventure-works.com/ ,"2345 Main St., Buffalo, NY 98052",Adventure Works,"January 19, 2004",4:26:00 PM,$249.99,#FFBEBADA,terry.adams

and so on. Just remember to restart Blend if you finish modifying the sample dataset.

Note: You don’t need to supply a sample value for all rows but be sure to include a comma then. Otherwise Blend won’t load the file correctly.

After restart I can create a new string property with my new Login format:

image

and bind it to a DataGrid or any other control:

image

Happy mocking! :-)

kick it on DotNetKicks.com

Tags: ,

Audio recorder Silverlight 4 sample

by ondrejsv 12. December 2009 14:48

[UPDATE 2010-08-28: I updated the sample to work with the final release of Silverlight 4. I also improved the design a little :-)]

Today I want to share with you my Audio Record sample using some new cool features coming in Silverlight 4. The sample demonstrates:

  • recording audio using the AudioCaptureDevice and CaptureSource classes, and saving it using an AudioSink object,
  • access to local folders on the computer,
  • simple commanding model using the new Command properties and ICommand interface,
  • changing appearance of a standard control (button) using its visual states.

Of course, full source code is attached at the end of the article.

The user interface is really simple:

image

What we want to achieve:

  1. three state audio recorder with three buttons “Record”, “Stop” and “Save”,
  2. the user first selects the “Record” buttons, the recorder will begin recording; the user clicks the “Stop” button when he is done with recording and finally selects the “Save” button to be prompted by a classic Save file dialog to save the record into a standard WAV file on his disc,
  3. command buttons should have a correct enabled/disabled logic,
  4. a status bar with text helping the user what to do or what is the recorder doing,

Disclaimer: Obviously I’m not the one gifted with graphical skills so don’t expect nifty animations here and there. It’s just a sample. :-)

Recording audio

An AudioCaptureDevice object represents a hardware device cable capturing sound. There may be more than one present and the user may select a default one in the new Silverlight 4 configuration tab:

image

You can get the default one through the CaptureDeviceConfiguration class:

var audioDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();

Then you use a CaptureSource object to capture any input from this source:

_captureSource = new CaptureSource() { AudioCaptureDevice = audioDevice };

Lastly, we need to connect a sink to the capture source. Sinks allow us to process incoming input from the source in any way we want – for example to store in a memory stream. For this we derive a new AudioSink (our is called MemoryAudioSink) and override the most important method – OnSamples:

protected override void OnSamples(long sampleTime, long sampleDuration, byte[] sampleData) { // New audio data arrived, write them to the stream. _stream.Write(sampleData, 0, sampleData.Length); }

To connect and start capturing the sound:

_sink = new MemoryAudioSink(); _sink.CaptureSource = _captureSource; _captureSource.Start()

Writing a WAV file

CaptureSource sends the captured sound to its sinks as raw PCM data. We need to add a standard WAV header to be usable for a user. This is done by the SavePcmToWav method:

public static void SavePcmToWav(Stream rawData, Stream output, AudioFormat audioFormat) { if (audioFormat.WaveFormat != WaveFormatType.Pcm) throw new ArgumentException("Only PCM coding is supported."); BinaryWriter bwOutput = new BinaryWriter(output); // Write down the WAV header. // Refer to http://technology.niagarac.on.ca/courses/ctec1631/WavFileFormat.html // for details on the format. // Note that we use ToCharArray() when writing fixed strings // to force using the char[] overload because // Write(string) writes the string prefixed by its length. // -- RIFF chunk bwOutput.Write("RIFF".ToCharArray()); // Total Length Of Package To Follow // Computed as data length plus the header length without the data // we have written so far and this data (44 - 4 ("RIFF") - 4 (this data)) bwOutput.Write((uint)(rawData.Length + 36)); bwOutput.Write("WAVE".ToCharArray()); // -- FORMAT chunk bwOutput.Write("fmt ".ToCharArray()); // Length Of FORMAT Chunk (Binary, always 0x10) bwOutput.Write((uint)0x10); // Always 0x01 bwOutput.Write((ushort)0x01); // Channel Numbers (Always 0x01=Mono, 0x02=Stereo) bwOutput.Write((ushort)audioFormat.Channels); // Sample Rate (Binary, in Hz) bwOutput.Write((uint)audioFormat.SamplesPerSecond); // Bytes Per Second bwOutput.Write((uint)(audioFormat.BitsPerSample * audioFormat.SamplesPerSecond * audioFormat.Channels / 8)); // Bytes Per Sample: 1=8 bit Mono, 2=8 bit Stereo or 16 bit Mono, 4=16 bit Stereo bwOutput.Write((ushort)(audioFormat.BitsPerSample * audioFormat.Channels / 8)); // Bits Per Sample bwOutput.Write((ushort)audioFormat.BitsPerSample); // -- DATA chunk bwOutput.Write("data".ToCharArray()); // Length Of Data To Follow bwOutput.Write((uint)rawData.Length); // Raw PCM data follows... // Reset position in rawData and remember its origin position // to restore at the end. long originalRawDataStreamPosition = rawData.Position; rawData.Seek(0, SeekOrigin.Begin); // Append all data from rawData stream into output stream. byte[] buffer = new byte[4096]; int read; // number of bytes read in one iteration while ((read = rawData.Read(buffer, 0, 4096)) > 0) { bwOutput.Write(buffer, 0, read); } rawData.Seek(originalRawDataStreamPosition, SeekOrigin.Begin); }

Simple commanding with a simple view model

I’m a fan of the new Model-View-ViewModel pattern as it allows you further properly structure your presentation layer while taking advantage of powerful WPF/Silverlight databinding. It wasn’t so simple to use it in Silverlight projects because it did not support commands as the full WPF stack did so you needed to resort to Prism or other frameworks.

However, with Silverlight 4 comes the ICommand interface:

public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); }

and new Command properties on ButtonBase-derived controls and the HyperLink control.

With this you can create your own viewmodel wrapping all your code interacting with the user interface and application logic in a single unit-testable class. The user interface will hook into your viewmodel class though public ICommand and dependency properties by leveraging databinding. Your code-behind will be almost empty!

To simplify things I created a SimpleCommand because all of my commands will just call a method on the viewmodel class and I will set their executability directly through a property:

public class SimpleCommand : ICommand { public Action ExecuteAction { get; set; } private bool _canExecute; public bool MayBeExecuted { get { return _canExecute; } set { if (_canExecute != value) { _canExecute = value; if (CanExecuteChanged != null) CanExecuteChanged(this, new EventArgs()); } } } #region ICommand Members public bool CanExecute(object parameter) { return MayBeExecuted; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { if (ExecuteAction != null) ExecuteAction(); } #endregion }

Let’s dig into the viewmodel class – the heart of the application. First some declarations:

public class RecorderViewModel : DependencyObject { private SimpleCommand _recordCommand; private SimpleCommand _playPauseCommand; private SimpleCommand _saveCommand; private MemoryAudioSink _sink; private CaptureSource _captureSource; private SaveFileDialog saveFileDialog = new SaveFileDialog() { Filter = "Audio files (*.wav)|*.wav" }; public SimpleCommand RecordCommand { get { return _recordCommand; } } public SimpleCommand PlayPauseCommand { get { return _playPauseCommand; } } public SimpleCommand SaveCommand { get { return _saveCommand; } } public static readonly DependencyProperty StatusTextProperty = DependencyProperty.Register("StatusText", typeof(string), typeof(RecorderViewModel), null); public string StatusText { get { return (string)GetValue(StatusTextProperty); } set { SetValue(StatusTextProperty, value); } }

The constructor initializes the three commands to just call their respective methods on the class:

public RecorderViewModel() { _recordCommand = new SimpleCommand() { MayBeExecuted = true, ExecuteAction = () => Record() }; _saveCommand = new SimpleCommand() { MayBeExecuted = false, ExecuteAction = () => SaveFile() }; _playPauseCommand = new SimpleCommand() { MayBeExecuted = false, ExecuteAction = () => PlayOrPause() }; var audioDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice(); _captureSource = new CaptureSource() { AudioCaptureDevice = audioDevice }; GoToStartState(); }

The last piece is to define the methods doing the hard work:

protected void Record() { if (!EnsureAudioAccess()) return; if (_captureSource.State != CaptureState.Stopped) return; _sink = new MemoryAudioSink(); _sink.CaptureSource = _captureSource; _captureSource.Start(); // Enable pause command, disable record command _playPauseCommand.MayBeExecuted = true; _recordCommand.MayBeExecuted = false; StatusText = "Recording..."; } protected void PlayOrPause() { if (_captureSource.State == CaptureState.Started) { _captureSource.Stop(); // Disable pause command, enable save command _playPauseCommand.MayBeExecuted = false; _saveCommand.MayBeExecuted = true; StatusText = "Recording finished. You may save your record."; } } protected void SaveFile() { if (saveFileDialog.ShowDialog() == false) { return; } StatusText = "Saving..."; Stream stream = saveFileDialog.OpenFile(); WavManager.SavePcmToWav(_sink.BackingStream, stream, _sink.CurrentFormat); stream.Close(); MessageBox.Show("Your record is saved."); GoToStartState(); }

Putting it all together

I put three buttons and one textblock into the default MainPage.xaml:

<Button Command="{Binding RecordCommand}" Grid.Column="0" HorizontalAlignment="Left" Name="button1" Template="{StaticResource RecordCommandStyle}"/>
<Button Content="Stop" Command="{Binding PlayPauseCommand}" Grid.Column="1" HorizontalAlignment="Left" Name="button3" Width="75" Template="{StaticResource StopCommandStyle}"/>
<Button Content="Save" Command="{Binding SaveCommand}" Grid.Column="2" HorizontalAlignment="Left" Name="button2" Width="57" Margin="0,0,0,-1" Template="{StaticResource SaveCommandStyle}"/>
<TextBlock Text="{Binding StatusText}" Name="textBlock1" Margin="8,5,-32,8" Grid.Row="1" Grid.ColumnSpan="4" Foreground="#FF13E3BC" FontSize="12" />

Note the bindings to the Command properties of the buttons and Text property of the textblock element (but this is nothing special). The whole viewmodel is then set to the page’s DataContext which is the only thing we do inside the code-behind:

RecorderViewModel ViewModel; public MainPage() { InitializeComponent(); ViewModel = new RecorderViewModel(); DataContext = ViewModel; }

That’s it. I didn’t show changing the appearance of the standard button controls using visual states here because this possibility has been with us long before the fourth version but it’s included in the project.

The floppy disk icon comes from the wonderful Crystal project.

Download full sources of the sample.

kick it on DotNetKicks.com

Tags:

Globalization is not an enemy

by ondrejsv 9. December 2009 18:36

System.FormatException: Input string was not in a correct format. The exception I see again and again because I’m running an English Windows 7 with Slovak regional settings and some developers just can’t imagine there are regions in the world where the decimal separator is not a period (.) but a comma (,) or where they use more than 12 hours a day. It’s unfortunate for them because many more people live outside the USA so reach of their application is far less than it could be.

Today I want to build a demonstration utilizing a Ribbon bar in my Silverlight application and I found one. A sample was attached but it crashed immediately after start with the aforementioned exception. The Ribbon control stores some default brushes in an XML file and parses on its own with code like this:

string xStr = pointAsString.Substring(0, pointAsString.IndexOf(","));
string yStr = pointAsString.Substring(pointAsString.IndexOf(",") + 1);

return new Point(double.Parse(xStr), double.Parse(yStr));

If we take a look at a fragment of the XML:

<LinearGradientBrush Name="GroupedButtonSemiHighlight" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#FFFEF7" Offset="0.0" />
<GradientStop Color="#FFF7DF" Offset="0.5" />
<GradientStop Color="#FFEEBE" Offset="0.6" />
<GradientStop Color="#FFF6DD" Offset="1.0" />
</LinearGradientBrush>

we see that it cannot succeed on any client configuration which does not use a point as decimal separator because the double.Parse method without providing an IFormatProvider would parse the string using the current regional settings.

If you write code that parses numbers from a content not edited or viewable directly by users and don’t want to take regional settings into account, always use the Invariant Culture as your IFormatProvider.

So I fixed the code:

string xStr = pointAsString.Substring(0, pointAsString.IndexOf(","));
string yStr = pointAsString.Substring(pointAsString.IndexOf(",") + 1);

return new Point(double.Parse(xStr, CultureInfo.InvariantCulture), double.Parse(yStr, CultureInfo.InvariantCulture));

More detailed information on internationalization, globalization and localization issues can be found on the Globalization Step By Step Microsoft site.

kick it on DotNetKicks.com

Tags:

Fighting the spam

by ondrejsv 22. November 2009 15:58

My blog is not immune to those pesky individuals sending their junk throughout the Internet universe. As the blogging engine I use (BlogEngine.NET) has no native way to block them, I was happy to find the Commentor plugin. Once installed, it will automatically check all incoming comments against the Akismet database (in order to use it you must register on the Wordpress and get their API key). You may also easily delete all junk comments posted by spammers before installing Commentor in its administration interface.

I highly recommend to install the Commentor or take another steps to remove spam from your blog because site with junk/regular post ratio 10:1 looks that its author lost all his interest in the site.

Tags:

How to specify UIHint template only for specific mode in ASP.NET Dynamic Data

by ondrejsv 8. October 2009 16:58

If you want to display a data field in your custom template instead of the default one coming with ASP.NET Dynamic Data, you can by decorating your metadata with the UIHint attribute:

[UIHint("Attributes")] public EntityCollection<Attribute> Attributes

In this case, Attributes collection will be displayed using Attributes.ascx template. Dynamic Date is clever enough to use Attributes_Edit.ascx if you happen to edit the collection. If Attributes_Edit.ascx does not exist, Dynamic Date will use the default template.

However, if you have a template that you want to use only for editing (or inserting) and use the default template otherwise (for read only), you cannot. If you supply the UIHint, you are required to have also a read-only template – in our case, you must have Attributes_Edit.ascx together with Attributes.ascx. You end up copying the content of the default template and renaming it (to Attributes.ascx and so on). As we know that copy-paste is a bad programmer’s friend which promotes low code maintainability and readability, we must come up with another solution.

Dynamic Data delegates deciding which template to use for which field to an instance of the FieldTemplateFactory class. In particular, we are interested in its GetFieldTemplateVirtualPath method which eats a column to get the template for, mode (ReadOnly, Edit, Insert) and UIHint value. We can, of course, extend the default factory to process UIHints in the form realUIHint|mode (e.g. “Attributes|Edit” which means use the Attributes_Edit.ascx for editing but the default template for anything else). The actual code is simple:

public class MyFieldTemplateFactory : FieldTemplateFactory { private const char HintSeparator = '|'; public override string GetFieldTemplateVirtualPath(MetaColumn column, System.Web.UI.WebControls.DataBoundControlMode mode, string uiHint) { if (!string.IsNullOrEmpty(uiHint) && uiHint.IndexOf(HintSeparator) > 0) { string[] sArr = uiHint.Split(HintSeparator); string hint = sArr[0]; string forMode = sArr[1]; // If current mode is the one specified in the UiHint // use it, otherwise fall back to the base implemenation. if (mode.ToString() == forMode) uiHint = hint; else uiHint = ""; } var temp = base.GetFieldTemplateVirtualPath(column, mode, uiHint); return base.GetFieldTemplateVirtualPath(column, mode, uiHint); } }

Associate your custom template factory with the model in the RegisterRoutes method inside the Global.asax:

model.FieldTemplateFactory = new MyFieldTemplateFactory();
Done! :-)
kick it on DotNetKicks.com

Tags: ,

Input string was not in a correct format when installing SQL Server 2008 with Reporting Services

by ondrejsv 5. October 2009 16:37

I’ve got a fresh Windows 7 copy on my notebook so reinstallation of all applications is a necessary evil. Microsoft SQL Server 2008 proved to be the most evil one.

After starting good old setup.exe I learned from a message box that this program is incompatible with Windows 7 and I must apply SP1 after installation. OK, I selected all features to install without any hesitation, Reporting Services including. Suddenly in the middle of the installation, an “Input string was not in a correct format” popped out at me but the installation was not rolled back and seemed to finish successfully.

Then I wanted to apply SP1 but I couldn’t because the SP1 detected a problem in a previous installation and refused any other work until I resolve it.

A quick look in the log file (C:\Program Files\Microsoft SQL Server\100\Setup Bootstrap\Log\DATE_OF_YOUR_INSTALL\Detail.txt):

2009-10-05 10:13:43 RS: System.FormatException: Input string was not in a correct format.
at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
at System.Int32.Parse(String s, IFormatProvider provider)
at System.Diagnostics.PerformanceCounterLib.GetStringTable(Boolean isHelp)
at System.Diagnostics.PerformanceCounterLib.get_NameTable()
at System.Diagnostics.PerformanceCounterLib.get_CategoryTable()
at System.Diagnostics.PerformanceCounterLib.CategoryExists(String machine, String category)
at System.Diagnostics.PerformanceCounterCategory.Exists(String categoryName, String machineName)
at System.Diagnostics.PerformanceCounterCategory.Exists(String categoryName)
at Microsoft.ReportingServices.Common.RSPerfCounterInstallUtil.Uninstall()
at Microsoft.SqlServer.Configuration.RSExtension.RSPerfCounterUtil.SetPerfCounters()
at Microsoft.SqlServer.Configuration.RSExtension.SQLRSConfigurationPrivate.Install_ConfigRC(Dictionary`2 actionData, PublicConfigurationBase spcb)
2009-10-05 10:13:43 Slp: Configuration action failed for feature RS_Server_Adv during timing ConfigRC and scenario ConfigRC.

Indeed, a problem with my performance counters. Start-run-perfmon and a message box that some of my counters could not be loaded.

First I wanted to sacrifice Reporting Services as I don’t really need them. But I was in a trap: removal failed with the exactly same message (input string…).

I searched the Internet and found a simple command to rebuild performance counters: “lodctr /R”. Just be sure to run it as administrator otherwise it would claim “Error 5”.

And voila – Reporting Services are uninstalled and SP1 installed. :-) (And I believe that you can install the RS now if you want)

Tags:

Connectors of Visio n-ary association cannot be right-angled

by ondrejsv 16. September 2009 12:59

This is my very first complaint on my blog but I cannot resist posting. I know that Microsoft Visio is not so popular tool when it comes to UML modeling (though for some diagramming is very useful). Nevertheless I think that its UML stencil could be little bit more friendly.

Today I wanted to draw an n-ary association on a complex diagram. To my surprise I found out that its connectors cannot be made right-angled as connectors of classical binary associations. You must even rotate the diamond shape to position them correctly. However you cannot freely move the role labels. The result is really ugly (I left only relevant shapes on the picture):

image

I wanted to edit the built-in shape but it’s so complicated (not even mentioning to make it work with Visio UML add-on) that I gave up.

You can instead place an ordinary diamond symbol and connect it by connectors with your classes but the association it represents won’t be stored in your model, of course.

This is how it should look (drawn in the Sparx Enterprise Architect):

image

Tags:

Data binding and formatting multiple values in Silverlight

by ondrejsv 16. September 2009 09:15

This article introduces a way to produce formatted strings with standard .NET formatting patterns (as used by the String.Format method) in data-binding on the Silverlight platform.

Oftentimes we require some kind of formatting when binding a value from a data source on the form. In ASP.NET many of the data-bound controls have the DataFormatString property in addition to the DataField property. But if you want to bind against more than one data field and concatenate their values into one formatted string, you usually need to create a template field. An example could be a Contact object from the AdventureWorks model with its FirstName and LastName properties which we want to concatenate and show in a single GridView column.

Windows Presentation Foundation features a more advanced framework for data-binding. However, there is no built-in support for formatting data-bound values. Fortunately, with extensibility in mind, WPF supports so-called converters which help to convert raw values coming from data sources into the final object consumable by the target property. They realize the IValueConverter interface:

public interface IValueConverter { object Convert(object value, Type targetType, object parameter, CultureInfo culture); object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture); }

or IMultiValueConverter interface for converters taking more than one input:

public interface IMultiValueConverter { object Convert(object[] values, Type targetType, object parameter, CultureInfo culture); object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture); }

To achieve our goal we can use one of the already built multi-value converters from the WPF Convertes project, FormatConverter:

...
<my:DataGridTextColumn Header="Name">
<my:DataGridTextColumn.Binding>
<MultiBinding Converter="{cx:FormatConverter {}{0} {1}}">
<Binding Path="Individual.Contact.FirstName" />
<Binding Path="Individual.Contact.LastName" />
</MultiBinding >
</my:DataGridTextColumn.Binding>
</my:DataGridTextColumn>

However, multi-value converters are not supported on the Silverlight platform, so it is not the WPF Convertes package.

To perform a similar string formatting we must write our own value converter. We make use of the parameter parameter of the IValueConverter.Convert method which is filled with any string supplied by the user in markup (through the ConverterParameter attribute of the Binding markup extension). We will support any number of input data fields and also property paths (to enable binding against object graphs). Our converter will be able to process format specified as in this example:

<my:DataGridTextColumn Header="Number" Binding="{Binding Converter={StaticResource MultiStringConverter}, ConverterParameter='[Individual.Contact.FirstName] [Individual.Contact.LastName]'}" />

Notice that data fields values of which we want to substitute, are enclosed in brackets (braces would reflect more nicely the syntax used by the String.Format method but we would need to escape them with a backslash – not so practical).

The code works by picking up all the property paths in the format string and assigning them unique index values. These indices are inserted to the rest of the format string. Then property paths are evaluated against the binding target (we use the code similar to the one in my article Binding object graphs to the ASP.NET GridView control) and passed to the String.Format method together with new format string. More detailed code description follows.

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Dictionary<string , paramvalueentity> paramValue = new Dictionary<string, ParamValueEntity>(); string formatString = (string)parameter; StringBuilder realFormatString = new StringBuilder(); int position = 0; string currentParam = ""; int lastIndex = 0;

We will store all found property paths in the format string together with their assigned index in a ParamValueEntity object (justs a wrapper for this pair). realFormatString variable stores the new format string.

We’ll proceed in a while loop by looking at the characters of the original format string (TagStart constant refers to '['):

while (position < formatString.Length) { if (formatString[position] == TagStart) { // We've just entered a parameter

… escaping is handled by repeating the left bracket…:

// Handle escaping (double TagStart) if (position+1<formatString.Length && formatString[position+1] == TagStart) { realFormatString.Append(TagStart); position += 2; continue; }

… otherwise we found beginning of a property path and we pick it up…:

position++; currentParam = ""; int startPosition = position; while ((char.IsLetterOrDigit(formatString[position]) || formatString[position] == '.') && position < formatString.Length) { position++; } currentParam = formatString.Substring(startPosition, position - startPosition);

… we check if such property path has already been found. If so, we put its index in the new format string (we evaluate a single property path only once – both for performance reasons but more importantly the property getter could have side effects and its repeated evaluations could return different values). If this is the first occurrence of the property path, we assign a new index value… :

// Have we met it already? ParamValueEntity pve; if (!paramValue.TryGetValue(currentParam, out pve)) { pve = new ParamValueEntity(); pve.Index = lastIndex++; paramValue[currentParam] = pve; }

… the we pick anything up to the finalizing right bracket (for example any formatting patterns – think of [Person.BirthDate:D])… :

// Parse up to the finalizing ']' startPosition = position; while (formatString[position] != TagEnd && position < formatString.Length) position++; string rest = ""; if (startPosition < position) rest = formatString.Substring(startPosition, position - startPosition);
... and put the index value to the new format string...:
// Add to the string builder realFormatString.Append("{" + pve.Index.ToString() + rest + "}"); // Pass the } if (position < formatString.Length) position++;

… all other characters are copied verbatim…:

} else { realFormatString.Append(formatString[position++]); } }

… then we evaluate all property paths…:

Type type = value.GetType(); // Let's find values foreach (var pvde in paramValue) { pvde.Value.Value = GetValueFromPropertyPath(value, pvde.Key); }

… and call the String.Format method on the new format string and just evaluated values…:

var q = from pve in paramValue.Values orderby pve.Index select pve.Value; return string.Format(realFormatString.ToString(), q.ToArray());

The GetValueFromPropertyPath method evaluates a property path against an object graph. It’s code is almost identical to the one described in my previous article (Binding object graphs to the ASP.NET GridView control)) but it uses pure Reflection instead of the TypeDescriptor and PropertyDescriptor wrappers as these are unavailable in Silverlight.

You can download the complete code together with a sample featuring a DataGrid showing some data from the AdventureWorks. Happy coding!

kick it on DotNetKicks.com vote it on WebDevVote.com

Tags: ,

Binding object graphs to the ASP.NET GridView control

by ondrejsv 1. September 2009 15:31

One of the drawbacks of the standard ASP.NET GridView control is that you can’t use the “dot” convention to bind object graphs when using the BoundField columns. This article explains how to do it with a new inherited column.

Suppose you have a typical object graph from the AdventureWorks model consisting of objects of the Order, OrderDetail, Product, ProductCategory, and ProductSubcategory classes as shown on the figure below. It’s not important how you get the graph, be it as a result of a LINQ-to-SQL or LINQ-to-Entities query or you have created it manually from a direct SQL query through ADO.NET. Our task is to create a simple grid of all items on a given order (represented by a collection of OrderDetail objects). The user is interested in the name of the product placed on the order, it’s category and subcategory and quantity ordered and unit price it was sold for. Note that the product name, category and subcategory are attributes of the Product object while quantity and unit price are attributes of the OrderDetail object itself. In the object world the standard way of getting information from “subordinate” objects is by traversing via object references (while in the relational world we would be issuing a JOIN statement).

image

If we were binding a single OrderDetail object to a FormView control, for example, we could use a perfectly valid Eval statements:

<%# Eval("Product.Name") %>
<%# Eval("Product.Subcategory.Name ") %>
<%# Eval("Product.Subcategory.Category.Name ") %>

However, a first try to use a BoundField column within a regular ASP.NET GridView control:

<asp:BoundField HeaderText="Product" DataField="Product.Name" />

results in the HttpException "field or property with the name 'Product.Name' was not found on the selected data source."

The problem is that a BoundField column looks up only for properties of the object it’s bound to and does not analyze the DataField string if it contains a navigation path to the property.

The solution is simple: in the attached sample project you find the NavigationBoundField class which inherits from the standard BoundField class and overrides the key method GetValue.

The code uses TypeDescriptors to retrieve object properties (via GetProperties method) and PropertyDescriptors to get value of a property (GetValue method).

Disclaimer: This article and its enclosed sample just demonstrates a single technical aspect. It by no means shows how to create a properly designed multi-layered application or how to write a properly designed code in a complex application.

kick it on DotNetKicks.com vote it on WebDevVote.com

Tags: