views:

158

answers:

2

Below is a simple WPF application which displays Customers with the most orders which is collected from the database via LINQ-to-SQL in the code-behind.

What is the best way to extend this WPF application so that the user can select from a dropdown, e.g.:

  • Customers with the most orders
  • Customers with the least orders
  • Customers by city
  • Customers by state

and the appropriate collection will be shown?

Here are some ideas:

  1. have many dockpanels with all the data loaded which are turned Visible/Collapse by the event handler (quick and dirty approach)
  2. have a ContentControl which is populated via DelegateCommand in a ViewModel which somehow fills the ContentControl with a ViewModel which is then attached to a View via a Converter in the XAML
  3. use a MVP pattern and have the Presenter fill the ContentControl with the appropriate View (bound to the Presenter's collections), this would allow you to only load the data right before it would be viewed
  4. or is there a way to do this in simple code-behind but where the data would only be loaded when the user chooses the selection in the drop down

How do you solve this common line-of-business issue in your WPF/Silverlight apps, i.e. customer clicks on control and an area of the screen shows him appropriate information?

XAML:

<Window x:Class="TestLinqToSql123.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="500">
    <Window.Resources>
        <DataTemplate x:Key="CustomerTemplate">
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}The customer {0} lives in {1} and has {2} orders.">
                        <Binding Path="Name"/>
                        <Binding Path="City"/>
                        <Binding Path="NumberOfOrders"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>

    </Window.Resources>
    <DockPanel>
        <Border 
            Background="Orange"
            CornerRadius="5"
            Margin="10"
            Padding="10">
            <ScrollViewer>
                <StackPanel>
                <TextBlock Text="Customers with the most orders:"
                   Margin="0 0 0 10"
                   FontSize="18"/>

                <ItemsControl x:Name="TheList"
                        ItemTemplate="{StaticResource CustomerTemplate}"/>
                </StackPanel>
            </ScrollViewer>
        </Border>
    </DockPanel>
</Window>

Code-Behind:

using System.Windows;
using TestLinqToSql123.Models;
using System.Linq;

namespace TestLinqToSql123
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            NorthwindDataContext db = new NorthwindDataContext();

            var customers = from c in db.Customers
                            orderby c.Orders.Count descending
                            select new
                            {
                                Name = c.ContactName,
                                c.City,
                                NumberOfOrders = c.Orders.Count
                            };
            TheList.ItemsSource = customers;
        }
    }
}
+2  A: 

I would use the Model-View-ViewModel pattern that is very popular in WPF and Silverlight applications.

Model

The customer class is an entity in the model. You also need a repository or service class to return collections of customers based on various criteria. In Silverlight data is retrieved asyncronously which complicates things a bit. By creating an interface or abstract base class for this repository or service class you are able to test the ViewModel class without using a database. You can instead mock some test data.

ViewModel

The ViewModel is connected to the view using data-binding and it has to either implement INotifyPropertyChanged and/or INotifyCollectionChanged or expose its data using dependency properties. The former approach is often easier to implement and test.

In your example it seems that you simply want to sort the same collection using various sort criteria. Your ViewModel is then a collection of customer objects. Displaying customers is quite simple and the ViewModel can contain customer entity objects directly. In more complex scenarios, e.g. where the customers can be edited you would want to create a customer view model.

Your ViewModel could either derive from ObservableCollection<T> or CollectionView. The later is class is not available in Silverlight and you will have to roll your own by implementing ICollectionView.

In addition to being a collection of customer objects your ViewModel would also need some methods to determine which sort order to use. Actually, it is probably smarter to add a SortOrder property to the ViewModel and resort the collection when this property changes.

One important point is that the ViewModel doesn't know about the user-interface and can be tested independently of the user-interface.

View

The View is a UserControl and is implemented using XAML. Some people find it very important to have no code-behind at all in the View, but the lack of commands in Silverlight requires either some code-behind or some command-like attached properties (PRISM has support for this in Silverlight).

The DataContext of the View is set to the ViewModel and elements of the View is then bound to the various parts of the ViewModel using data-binding. Buttons and other active user-interface controls are hooked up to methods etc. on the ViewModel. In WPF you can use commands, but in Silverlight you need some code-behind or something similar to commands.

When a user clicks on a control in the View the action is routed to the ViewModel either as a call to a method or a change in a property. The ViewModel is then updated and the data-binding from the View to ViewModel ensures that updated information is displayed to the user.

Martin Liversage
+1  A: 

You can do this in simple code-behind very easily. Add the following combo above your border.

<ComboBox 
    x:Name="SelectionCombo" 
    DockPanel.Dock="Top" 
    SelectionChanged="ComboBox_SelectionChanged"
    >
    <ComboBoxItem x:Name="MostOrders">Customers with the most orders</ComboBoxItem>
    <ComboBoxItem x:Name="LeastOrders">Customers with the least orders</ComboBoxItem>
    <ComboBoxItem x:Name="ByCity">Customers by city</ComboBoxItem>
    <ComboBoxItem x:Name="ByState">Customers by state</ComboBoxItem>
</ComboBox>

Then in your code behind add the following event handler

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ComboBoxItem item = (ComboBoxItem)e.AddedItems[0];
    if (item == MostOrders)
        GetByMostOrders();
    else if (item == LeastOrders)
        GetByLeastOrders();
    else if (item == ByCity)
        GetByCity();
    else if (item == ByState)
        GetByState();
}

And then create the methods that fills the combo

private void GetByMostOrders()
{
    NorthwindDataContext db = new NorthwindDataContext();

    var customers = from c in db.Customers
                    orderby c.Orders.Count descending
                    select new
                    {
                        Name = c.ContactName,
                        c.City,
                        NumberOfOrders = c.Orders.Count
                    };
    TheList.ItemsSource = customers;
}

If your looking for a more sophisticated approach I would probably follow Martin's advice, otherwise this should get you started.

Ian Oakes