Today I’m going to talk about c# language and .net framework. In fact, the platform becomes more popular from year to year. In spite of negative labels, which c# embodies, one of them for instance says: something that has been done with c# is in rough and ready fashion :), c# brings really worth features for professional long term development. More features came with .net 3.0 and c# 3.0 (.net 3.5).
Here I want to explain the global code writing strategies I adhere, and which IMHO implement more efficient, robust solutions using c# 3.0 and WPF.
Event-driven approach
Consider next example:
class Class1
{
private List<int> _collection;
private int _sum = 0;
public int Sum { get { return _sum; } }
public Class1()
{
_collection = new List<int>();
}
public void Add(int value)
{
_collection.Add(value);
_sum += value;
}
}
class Class2
{
private ObservableCollection<int> _collection;
private int _sum = 0;
public int Sum { get { return _sum; } }
public Class2()
{
_collection = new ObservableCollection<int>();
_collection.CollectionChanged += _collection_CollectionChanged;
}
public void Add(int value)
{
_collection.Add(value);
}
private void _collection_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
for (int i = 0; i < e.NewItems.Count; i++)
_sum += (int)e.NewItems[i];
}
}
Both classes hold a collection of values and a field that depend on the collection contents. The field should not be calculated each time when its value is used due to performance reasons – on default a field is accessed more often when a collection is changed and an operation of recalculating costs. So the efficient way here is to track collection changes. The example simplified as possible – instead of sum a field or its dependency from a collection can be more complex. So the task is real, and there are two solutions here. The first style I name classic object-oriented approach, the second - event-driven approach. Both approaches are wide-spread in c#. Consider pluses and minuses right now.
On the first look it makes sense that 2nd approach is better. You will never forget about updating a dependency field because it occurs in one place and looks clear. However there are a lot of consequences here. In fact, a collection is responsible now for a class field, not a class itself. Essentially this means that a class and its internal collection are highly coupled and it violates object-oriented crucial design principle – component modularity. Secondly, code workflow became unobvious – call stack is broken. Thirdly, extensibility is lame. If such module becomes more complex it will be harder to update it, because we have high coupling and vague workflow.
Trying to be objective I must say that the 1st approach is preferable.
Property ambiguity
class ValueHolder
{
public int Value { get; set; }
}
class ClassWithProperty
{
private ValueHolder _value;
public ValueHolder Value
{
get
{
if (_value == null)
_value = new ValueHolder();
return _value;
}
set
{
_value = value;
if (_value.Value < 0)
_value.Value = 0;
}
}
public void DoSomeStuff()
{
//ambiguity to use Value or _value ?
}
}
Here the public property and the private field have the same meaning, but however are different in usage. While a code will grow it is important to resolve this ambiguity. The code should be refactored to:
class ValueHolder
{
private int _value;
public int Value
{
get { return _value; }
set
{
_value = value;
if (_value < 0)
_value = 0;
}
}
}
class ClassWithProperty
{
private ValueHolder _value = new ValueHolder();
public ValueHolder Value
{
get { return _value; }
set { _value = value; }
}
public void DoSomeStuff()
{
//Value or _value
}
}
So property is the good way to add extra emerging logic to an exposing field, but the better way is not to abuse this feature (except the cases when a property exposes a field with a slightly or considerably different meaning).
Considered stuff belongs to any version of c#. Next part is devoted to modern c# 3.0 and WPF.
XAML and ViewModel pattern
Exposing BAL classes to GUI is the known problem of mixing and messing them with GUI entities. WPF gives the trendy solution – ViewModel pattern (Model-View-ViewModel or MVVM).
Here you can find the explanation of pattern
Again the idea is that you build up ViewModel classes which expose future displaying entities. In other words you build up abstract view (its data context) or model extended for presentation. Next step is xaml with its binding and commands which make ViewModel attaching easy. XAML also has other profits; it enables rapid View creating/maintaining and leads to minimal code in xaml.cs files.
MVVM exampleOn the left side there is editable data grid, on the right saving data. It looks like working with an xml file in WPF, however it does not. In the example there is extendable MVVM, which connects left and right parts in some common manner.
The Model simply holds the cars collection, the operations on it and the xml features.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.IO;
namespace WPFExample
{
public class CarsModel
{
public class Car
{
public Car()
{
Year = DateTime.Now.Year;
Color = "255,0,0,0";
}
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Year { get; set; }
public string Color { get; set; }
}
public CarsModel()
{
_xml = @"
<Cars>
<Car>
<Id>1</Id>
<Name>Volvo 850</Name>
<Description>car details</Description>
<Year>1994</Year>
<Color>255,0,0,0</Color>
</Car>
<Car>
<Id>2</Id>
<Name>Opel Vectra</Name>
<Description>details</Description>
<Year>1999</Year>
<Color>255, 212, 208, 200 </Color>
</Car>
</Cars>
";
XDocument document = XDocument.Load(new StringReader(_xml));
_cars = (from car in document.Descendants("Car")
select new Car
{
Id = (int)car.Element("Id"),
Name = (string)car.Element("Name"),
Description = (string)car.Element("Description"),
Year = (int)car.Element("Year"),
Color = (string)car.Element("Color"),
}).ToList();
_lastId = (from car in _cars select car.Id).Max();
UpdateXml();
}
private int _lastId;
private readonly List<Car> _cars;
private string _xml;
public List<Car> Cars
{
get { return _cars; }
}
public string Xml
{
get { return _xml; }
}
public void UpdateXml()
{
_xml = new XElement("Cars",
from car in _cars
select new XElement("Car",
new XElement("Id", car.Id),
new XElement("Name", car.Name),
new XElement("Description", car.Description),
new XElement("Year", car.Year),
new XElement("Color", car.Color))).ToString();
}
public void Insert(Car car)
{
car.Id = ++_lastId;
_cars.Add(car);
}
public void Update(Car car)
{
Car internalCar = _cars.Find(c => c.Id == car.Id);
if (internalCar != null)
{
internalCar.Name = car.Name;
internalCar.Description = car.Description;
internalCar.Year = car.Year;
internalCar.Color = car.Color;
}
}
public void Delete(Car car)
{
_cars.RemoveAll(c => c.Id == car.Id);
}
}
}
The CarsViewModel and CarViewModel constructors initialize all data and commands at once. It became possible through the Delegate Command feature from
Composite WPF library. Make attention on the constructors again – they require handlers – I think it’s very powerful approach to show both data and actions a ViewModel presents.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.ComponentModel;
using System.Windows.Media;
using Microsoft.Practices.Composite.Wpf.Commands;
using System.Windows;
using System.Collections.Specialized;
namespace WPFExample
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public delegate string CarHandler(CarsModel.Car car);
public class CarsViewModel : ViewModel
{
public CarsViewModel(
IEnumerable<CarsModel.Car> cars,
string xml,
CarHandler insertCarHandler,
CarHandler updateCarHandler,
CarHandler deleteCarHandler)
{
_cars = new ObservableCollection<CarViewModel>(
(from car in cars
select new CarViewModel(car, (c) =>
{
Xml = updateCarHandler(c);
return Xml;
})).ToList());
_cars.CollectionChanged += (o, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (CarViewModel car in e.OldItems)
{
Xml = deleteCarHandler((CarsModel.Car)car);
}
}
};
Xml = xml;
Copy = new DelegateCommand<object>(
(o) =>
{
Clipboard.SetText(Xml);
});
Insert = new DelegateCommand<object>(
(o) =>
{
CarsModel.Car car = new CarsModel.Car();
Xml = insertCarHandler(car);
CarViewModel carViewModel = new CarViewModel(car, (c) =>
{
Xml = updateCarHandler(c);
return Xml;
});
_cars.Add(carViewModel);
});
Delete = new DelegateCommand<object>(
(o) =>
{
if (SelectedCars != null)
{
IList deletionList = new List<CarViewModel>(
SelectedCars.Cast<CarViewModel>());
foreach (CarViewModel car in deletionList)
{
_cars.Remove(car);
Xml = deleteCarHandler((CarsModel.Car)car);
}
}
},
(o) =>
{
return _selectedCars != null && _selectedCars.Count > 0;
});
_years = new List<int>();
for (int i = 1970; i <= DateTime.Now.Year; i++)
_years.Add(i);
}
private readonly ObservableCollection<CarViewModel> _cars;
public ObservableCollection<CarViewModel> Cars { get { return _cars; } }
private IList _selectedCars;
public IList SelectedCars
{
get { return _selectedCars; }
set
{
_selectedCars = value;
Delete.RaiseCanExecuteChanged();
}
}
public DelegateCommand<object> Copy { get; set; }
public DelegateCommand<object> Insert { get; set; }
public DelegateCommand<object> Delete { get; set; }
private string _xml;
public string Xml
{
get { return _xml; }
private set
{
_xml = value;
NotifyPropertyChanged("Xml");
}
}
private readonly List<int> _years;
public List<int> Years { get { return _years; } }
public class CarViewModel : ViewModel
{
public int Id { get; set; }
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
private string _description;
public string Description
{
get { return _description; }
set
{
_description = value;
NotifyPropertyChanged("Description");
}
}
private int _year;
public int Year
{
get { return _year; }
set
{
_year = value;
NotifyPropertyChanged("Year");
}
}
private Color _color;
public Color Color
{
get { return _color; }
set
{
_color = value;
NotifyPropertyChanged("Color");
}
}
public byte ColorR
{
get { return _color.R; }
set
{
_color.R = value;
NotifyPropertyChanged("Color");
}
}
public byte ColorG
{
get { return _color.G; }
set
{
_color.G = value;
NotifyPropertyChanged("Color");
}
}
public byte ColorB
{
get { return _color.B; }
set
{
_color.B = value;
NotifyPropertyChanged("Color");
}
}
public string ColorSortableString
{
get { return string.Format("{0:x2}{1:x2}{2:x2}", _color.R, _color.G, _color.B); }
}
public CarViewModel(
CarsModel.Car car,
CarHandler updateCarHandler)
{
Id = car.Id;
Name = car.Name;
Description = car.Description;
Year = car.Year;
string[] bytes = car.Color.Split(',');
Color = Color.FromArgb(byte.Parse(bytes[0]), byte.Parse(bytes[1]), byte.Parse(bytes[2]), byte.Parse(bytes[3]));
PropertyChanged += (o, e) =>
{
updateCarHandler((CarsModel.Car)this);
};
}
public static explicit operator CarsModel.Car(CarViewModel car)
{
return new CarsModel.Car
{
Id = car.Id,
Name = car.Name,
Description = car.Description,
Year = car.Year,
Color = string.Format("{0},{1},{2},{3}", car.Color.A, car.Color.R, car.Color.G, car.Color.B),
};
}
}
public enum ColumnType
{
String,
Year,
Color,
}
public class ColumnDefinition
{
public string Name { get; set; }
public string Path { get; set; }
public string SortMemberPath { get; set; }
public ColumnType Type { get; set; }
}
public ColumnDefinition[] Columns
{
get
{
var columns = new List<ColumnDefinition>
{
new ColumnDefinition
{
Name = "Title",
Path = "Name",
SortMemberPath = "Name",
Type = ColumnType.String,
},
new ColumnDefinition
{
Name = "Description",
Path = "Description",
SortMemberPath = "Description",
Type = ColumnType.String,
},
new ColumnDefinition
{
Name = "Year",
Path = "Year",
SortMemberPath = "Year",
Type = ColumnType.Year,
},
new ColumnDefinition
{
Name = "Color",
Path = "Color",
SortMemberPath = "ColorSortableString",
Type = ColumnType.Color,
}
};
return columns.ToArray();
}
}
}
}
XAML presents the View structure.
<Window x:Class="WPFExample.CarsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WpfToolkit"
xmlns:local="clr-namespace:WPFExample"
xmlns:mvvm="clr-namespace:MVVm.Core.View;assembly=MVVm.Core"
Title="Cars View" Width="900" Height="600">
<Window.Resources>
<mvvm:CommandReference x:Key="Insert" Command="{Binding Insert}"/>
</Window.Resources>
<Window.InputBindings>
<KeyBinding Command="{StaticResource Insert}" Key="Insert"/>
</Window.InputBindings>
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<local:BrushConverter x:Key="BrushConverter"/>
<DataTemplate x:Key="StringCell" DataType="String">
<TextBlock Text="{Binding Path}"/>
</DataTemplate>
<DataTemplate x:Key="StringCellEditing" DataType="String">
<TextBox Text="{Binding Path, Mode=TwoWay}"/>
</DataTemplate>
<DataTemplate x:Key="YearCell" DataType="Year">
<TextBlock/>
</DataTemplate>
<DataTemplate x:Key="YearCellEditing" DataType="Year">
<ComboBox ItemsSource="{Binding Path=DataContext.Years,
RelativeSource={RelativeSource AncestorType={x:Type toolkit:DataGrid}}}"
SelectedItem="{Binding Path, Mode=TwoWay}"/>
</DataTemplate>
<DataTemplate x:Key="ColorCell" DataType="Color">
<Border Background="{Binding Path, Converter={StaticResource BrushConverter}}"/>
</DataTemplate>
<DataTemplate x:Key="ColorCellEditing" DataType="Color">
<Border Background="{Binding Path, Converter={StaticResource BrushConverter}}">
<StackPanel Orientation="Vertical">
<Slider Value="{Binding Path, Mode=TwoWay}"
Minimum="0" Maximum="255"/>
<Slider Value="{Binding Path, Mode=TwoWay}"
Minimum="0" Maximum="255"/>
<Slider Value="{Binding Path, Mode=TwoWay}"
Minimum="0" Maximum="255"/>
</StackPanel>
</Border>
</DataTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical">
<toolkit:DataGrid x:Name="_dataGrid" ItemsSource="{Binding Cars}"
AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="True">
</toolkit:DataGrid>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding Insert}" Content="+"
Margin="5,5,0,0" Padding="3,3,3,3"/>
<Button Command="{Binding Delete}" Content="-"
Margin="5,5,0,0" Padding="3,3,3,3"/>
</StackPanel>
</StackPanel>
<Grid Grid.Column="1" Margin="1,1,1,1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border BorderBrush="Blue" BorderThickness="2" Grid.RowSpan="2"/>
<Button Command="{Binding Copy}" Content="Copy To Buffer"/>
<ScrollViewer Grid.Row="1">
<TextBlock Text="{Binding Xml}"/>
</ScrollViewer>
</Grid>
</Grid>
</Window>
The View code does developer-defined dynamic data grid initialization. It’s rather complex feature, however I didn’t want to do a simple example. There is only one callback in the View code that refreshes data grid selected rows in the ViewModel. The data grid control is from
WPF Toolkit. I’m sure that with another or own control implementation and with other XAML extensions like binding delegates to built-in commands it will be possible to rid from the View code at all.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using Microsoft.Windows.Controls;
using System.Windows.Input;
namespace WPFExample
{
public partial class CarsView : Window
{
public CarsView()
{
InitializeComponent();
DataContextChanged += (o, e) =>
{
_dataGrid.Columns.Clear();
Bind(this, ApplicationCommands.Delete, (DataContext as CarsViewModel).Delete);
Bind(this, ApplicationCommands.Copy, (DataContext as CarsViewModel).Copy);
foreach (CarsViewModel.ColumnDefinition columnDefinition in
(DataContext as CarsViewModel).Columns)
{
DataGridColumn column = CreateColumn(columnDefinition);
if (column != null)
_dataGrid.Columns.Add(column);
}
_dataGrid.SelectionChanged += (ob, ea) =>
{
(DataContext as CarsViewModel).SelectedCars = _dataGrid.SelectedItems;
};
};
}
static private void Bind(UIElement element, ICommand command, ICommand targetCommand)
{
element.CommandBindings.Add(new CommandBinding(command,
(o, e) =>
{
targetCommand.Execute(e.Parameter);
},
(o, e) =>
{
e.CanExecute = targetCommand.CanExecute(e.Parameter);
}));
}
private DataGridColumn CreateColumn(CarsViewModel.ColumnDefinition columnDefinition)
{
DataGridTemplateColumnWithBindingPath column = null;
switch (columnDefinition.Type)
{
case CarsViewModel.ColumnType.String:
column = new DataGridTemplateColumnWithBindingPath
{
CellTemplate = LayoutRoot.Resources["StringCell"] as DataTemplate,
CellEditingTemplate = LayoutRoot.Resources["StringCellEditing"] as DataTemplate,
};
break;
case CarsViewModel.ColumnType.Year:
column = new DataGridTemplateColumnWithBindingPath
{
CellTemplate = LayoutRoot.Resources["YearCell"] as DataTemplate,
CellEditingTemplate = LayoutRoot.Resources["YearCellEditing"] as DataTemplate,
};
break;
case CarsViewModel.ColumnType.Color:
column = new DataGridTemplateColumnWithBindingPath
{
CellTemplate = LayoutRoot.Resources["ColorCell"] as DataTemplate,
CellEditingTemplate = LayoutRoot.Resources["ColorCellEditing"] as DataTemplate,
};
break;
}
if (column != null)
{
column.Header = columnDefinition.Name;
column.CanUserReorder = true;
column.CanUserSort = true;
column.SortMemberPath = columnDefinition.SortMemberPath;
column.BindingPath = columnDefinition.Path;
}
return column;
}
}
public class DataGridTemplateColumnWithBindingPath : DataGridTemplateColumn
{
public string BindingPath
{
get { return (string)GetValue(BindingProperty); }
set { SetValue(BindingProperty, value); }
}
public static readonly DependencyProperty BindingProperty = DependencyProperty.Register(
"BindingPath", typeof(string), typeof(DataGridTemplateColumnWithBindingPath));
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
FrameworkElement element = base.GenerateEditingElement(cell, dataItem);
element.ApplyTemplate();
SetEditingBindingPath(VisualTreeHelper.GetChild(element, 0));
return element;
}
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
FrameworkElement element = base.GenerateElement(cell, dataItem);
element.ApplyTemplate();
SetBindingPath(VisualTreeHelper.GetChild(element, 0));
return element;
}
private void SetEditingBindingPath(object bindingElement)
{
string dataType = (string)CellEditingTemplate.DataType;
if (dataType == "String")
{
ResetBindingWithNewPath(bindingElement as TextBox,
TextBox.TextProperty, BindingPath);
}
else if (dataType == "Year")
{
ResetBindingWithNewPath(bindingElement as ComboBox,
ComboBox.SelectedItemProperty, BindingPath);
}
else if (dataType == "Color")
{
Border border = bindingElement as Border;
ResetBindingWithNewPath(border, Border.BackgroundProperty, BindingPath);
ResetBindingWithNewPath(((border.Child as StackPanel).Children[0]
as Slider), Slider.ValueProperty, BindingPath + "R");
ResetBindingWithNewPath(((border.Child as StackPanel).Children[1]
as Slider), Slider.ValueProperty, BindingPath + "G");
ResetBindingWithNewPath(((border.Child as StackPanel).Children[2]
as Slider), Slider.ValueProperty, BindingPath + "B");
}
}
private void SetBindingPath(object bindingElement)
{
string dataType = (string)CellTemplate.DataType;
if (dataType == "String" || dataType == "Year")
{
ResetBindingWithNewPath(bindingElement as TextBlock,
TextBlock.TextProperty, BindingPath);
}
else if (dataType == "Color")
{
ResetBindingWithNewPath(bindingElement as Border,
Border.BackgroundProperty, BindingPath);
}
}
private static void ResetBindingWithNewPath(
FrameworkElement element,
DependencyProperty property,
string newPath)
{
BindingExpression binding = element.GetBindingExpression(property);
element.SetBinding(property,
new Binding
{
Path = new PropertyPath(newPath),
Mode = binding != null ? binding.ParentBinding.Mode : BindingMode.Default,
Converter = binding != null ? binding.ParentBinding.Converter : null,
ConverterParameter = binding != null ? binding.ParentBinding.ConverterParameter : null,
});
}
}
public class BrushConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new SolidColorBrush((Color)value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
Finally here is the application initialization.
CarsModel model = new CarsModel();
CarsViewModel viewModel = new CarsViewModel(
model.Cars,
model.Xml,
(c) => { model.Insert(c); model.UpdateXml(); return model.Xml; },
(c) => { model.Update(c); model.UpdateXml(); return model.Xml; },
(c) => { model.Delete(c); model.UpdateXml(); return model.Xml; });
CarsView view = new CarsView { DataContext = viewModel };
view.Show();
Anonymous methods and lambda expressions
Writing callbacks always annoys me. It needs extra time for documenting/marking. Reading callbacks causes a reader leaping through different part of code while statements inside a callback should be read together with the callback usage statements. Since C# 2.0 there is the solution (anonymous methods) that lets to do inline callback implementation. C# 3.0 introduced lambda expressions - new simpler syntax. However anonymous methods can be still in use, because they have some features that lambda expressions have not. In practice all callbacks becomes inline and in rare cases, when similar callbacks contains the same code, this code should be a candidate for normally introduced method.
In the last example you can find usage of lambdas.
Deeper in MVVM
MVVM didn’t appear suddenly. Heretofore there were similar MVC (Model-View-Controller), MVP (Model-View-Presenter) patterns, which are not simple code-behind. Also data binding feature in ASP.NET looks like XAML binding. However there are two points which excel MVVM among all ancestors.
First is that de facto MVVM has language native support – there are no third-parties, plug-in classes and namespaces - no interfaces you should learn first. But of course you should be aware of XAML (the key to this support) and however learn a couple of interfaces. One of them is INotifyPropertyChanged interface, which always ought to be implemented in a ViewModel. It creates and routes developer-defined updating notifications to WPF internal engine. ObservableCollection and its descendants also have similar purpose - describing more complex data structure to WPF internal. There is no necessity to use it outside WPF.
Second point belongs to desktop and desktop-like (Silverlight) applications. Interaction between View and ViewModel now looks as simple as possible. Actually on the pattern schema there is no visible interaction arrow coming from View. It gives unit testing benefit - ViewModel can be tested fully independently and preserves minimal connection efforts.
Of course, the backlashes exist. One of them for instance says that MVVM costs considerable system resources and performance suffers due to declarative nature. Another says that the ViewModel does not directly describe the View and in large enterprise applications maintaining ViewModels adds difficulties. In spite of these arguments I must say that maximum performance and efficient usage of system resources are not major characteristics of UI. While code will grow it should be regularly managed. So problems with large applications I would refer to common problems. It’s the second question how MVVM can help or not.
In the last example you can see that the ViewModel is abstract (independent from the View), but however knows a lot about what the View is going to do. For instance, the ViewModel watches over deleted items from the cars collection outside itself and offers editable RGB color components. It would be better to take this code from the ViewModel to the View like the BrushConverter code. Doing this sometimes can be possible, but then can bring unprofitable, rather complicated solution. In this regard a ViewModel doesn’t form pure abstraction from its View. Perhaps it will be possible while XAML offers new instruments.
Linq-to-object and another syntax sugar
C# 3.0 has other features like linq-to-object, implicitly typed local variables, object and collection initializers, anonymous types, automatically implemented properties, etc. It is essential part of programming with C# 3.0, but it affects only syntax, not concepts. That’s why I didn’t bring it to front of this post. The only thing I must say that it really maintains the Microsoft slogan - .NET framework serves the purpose ‘What to code’ and not ‘How to code’. In practice code becomes more compact and after some programming experience easier to read and write. See the last example with Linq and other features.
Conclusion
In this post I’ve explained programming with C# 3.0 and WPF. My approach is using ViewModel pattern and most of C# 3.0 and XAML opportunities and not using event-driven style and callbacks. The style presents mix of imperative programming, where a sequence of commands is defined straight forward up to a ViewModel, and declarative syntax of C# 3.0, which improves reading and compact size.