Click here to Skip to main content
16,014,765 members
Articles / Desktop Programming / WPF
Article

Inside xamSalesManager: a NetAdvantage for WPF Showcase Sample

22 Jul 2008CPOL11 min read 55.1K   34  
Infragistics dives deep into the technical architecture of their xamSalesManager showcase sample for WPF. Read how you can apply the Model - View - ViewModel pattern to your own application designs to create compelling user experiences like in this sophisticated sales dashboard.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

Image 1

Table of Contents

Introduction

This document examines xamSalesManager, a WPF showcase sample by Infragistics released as part of the NetAdvantage for WPF 2008 Vol. 1. xamSalesManager is hosted in the Infragistics xamShowcase application, which is included in the NetAdvantage for WPF installation and can be run over the Web as an XBAP.

What is a Showcase Sample?

Infragistics includes two types of samples with each of its products: feature samples and showcase samples. Feature samples show specific features of individual controls, such as enabling row summaries on the data grid. Showcase samples demonstrate a number of controls working together in a real-world application context, such as a dashboard for regional sales managers.

Showcase Sample Goals

As of NetAdvantage for WPF 2008 Vol. 1, the showcase samples have taken on the new role of providing architectural guidance, as well as showing the style and performance capabilities of the Infragistics WPF controls. Starting with xamSalesManager, each new showcase sample demonstrates standard and innovative ways to use the Infragistics WPF component suite in a line-of-business application. In addition, the showcase samples are in accordance with industry best practices, utilizing proven design and User Experience (UX) patterns.

Showcase samples are not fully functional applications, but, rather, provide just enough functionality to demonstrate the components, UI patterns, and interaction designs presented. Any non-functional UI elements display a message when you interact with them, to help avoid confusion.

The end goal is to demonstrate how to leverage NetAdvantage for WPF for creating an excellent user experience in any WPF application.

What is xamSalesManager?

xamSalesManager is an executive dashboard application built for the Regional Sales Manager of a fictitious company. It provides high-level reporting information about the territories in her sales region, and information about each salesperson. The application displays Key Performance Indicator (KPI) information in both a tabular layout (i.e. a data grid) and in charts. It allows the user to compare the sales performance metrics of any salespeople in the same chart. It also provides the ability to view aggregated performance summaries in the data grid.

Below is a screenshot of xamSalesManager immediately after you load it up:

image001.jpg

Figure 1 - xamSalesManager starts up by presenting information about the company’s salespeople.

UI Architecture

Infragistics pays close attention to ensuring that it is easy to use the NetAdvantage for WPF components together seamlessly. This guarantees that we can create a rich user interface, achieve a consistent visual style, as well as incorporate Microsoft’s standard controls with ease. This section of the document reviews the UI components used in the xamSalesManager interface, and shows how they combine to create an appealing user experience. xamSalesManager makes use of the following four NetAdvantage for WPF components:

  • xamDockManager™
  • xamDataGrid™
  • xamChart™
  • xamRibbon™

The next several sections review how the UI uses each of these components.

xamDockManager

When you load the application, it starts by displaying four xamCharts. The user can open and view as many salesperson performance charts as they want at any time. This application relies on the xamDockManager to organize all of those xamCharts. Each xamChart exists in a separate pane of the xamDockManager, giving the user complete freedom over where the charts belong and how big they should become.

The xamDockManager seen below has the “Office2k7Black” theme applied to its panes:

image002.jpg

Figure 2 – The xamDockManager hosting several xamCharts.

The application has very little code that touches the xamDockManager. In most scenarios, you can consume the component entirely from XAML, without writing a line of code. The xamSalesManager application dynamically creates docking panes, which is a task is natural to perform in code. Here is the method in MainPage.xaml.cs responsible for that task, which executes when the user clicks on a salesperson’s name in the xamDataGrid:

C#
void ShowSalesPersonDetailsChart(SalesPersonViewModel salesPersonView) 
{ 
    SalesPersonControl salesPersonControl;

    if (_activeSalesPeopleMap.ContainsKey(salesPersonView)) { 
    salesPersonControl = _activeSalesPeopleMap[salesPersonView]; 
    } else { 
        salesPersonControl = new SalesPersonControl(); 
        _activeSalesPeopleMap.Add(salesPersonView, salesPersonControl);

        salesPersonControl.DataContext = salesPersonView;

        string header = salesPersonView.Name; 
        if (salesPersonView.IsTopPerformerInRegion) 
            header = String.Format("{0} (Top Performer)", header);

        ContentPane pane = new ContentPane { 
            Content = salesPersonControl, 
            TabHeader = salesPersonView.Name, 
            Header = header, 
            Image = new BitmapImage( 
                new Uri("/SalesManager/View/Icons/user.png", 
                UriKind.RelativeOrAbsolute)) 
        };

        pane.Closed += delegate { 
            _activeSalesPeopleMap.Remove(salesPersonView);

            if (_activeSalesPeopleMap.Count == 0) 
                this.ActiveSalesPerson = null; 
        };

        this.salespeoplePane.Items.Add(pane); 
    }

    salesPersonControl.BringIntoView(); 
    ContentPane cp = LogicalTreeHelper.GetParent(salesPersonControl) as ContentPane; 
    if (cp != null) 
        cp.Activate(); 
}

That method finds, or creates, a SalesPersonControl to display in a ContentPane. It then calls BringIntoView on the pane to ensure that it is visible in the UI. After that, it walks up the logical tree one node to find the ContentPane that hosts the control. It calls Activate on the pane to ensure that it becomes the xamDockManager’s ActivePane. The salesperson in the ActivePane is given a contextual tab in the xamRibbon, as we will examine later.

Learn more about the xamDockManager. 

xamDataGrid

The application displays its list of salespeople in a xamDataGrid. That grid provides aggregated financial data via row summaries. By default, the “Sales YTD” (Sales Year-to-Date) field displays the sum of all sales made by all salespeople in the sales region. The user can add more summaries by clicking on that field’s Row Summary dropdown in the field’s header, and selecting more options from the list of available summaries. Clicking on a salesperson’s name opens their Sales Performance chart. Checking the box in the first cell of a salesperson’s row adds that person to the Salesperson Comparison chart.

image003.jpg

Figure 3 – The xamDataGrid displaying salespeople grouped by Territory, with row summaries for total sales.

In the preceding screenshot, you will notice all salespeople are grouped by their Territory field. In the group-by header row, you can see the total sales in that territory. The row summary at the bottom of the xamDataGrid shows the total sales for the entire sales region.

Cells in the ‘Salesperson Name’ field display a hyperlink that, when clicked, causes the associated salesperson’s performance chart to open. Here is the declaration of that Field, in the DirectReportsControl.xaml file:

XML
<igDP:Field Label=&quot;Salesperson Name&quot; Name=&quot;Name&quot; IsScrollTipField=&quot;True&quot;> 
    <igDP:Field.Settings> 
        <igDP:FieldSettings 
            AllowGroupBy=&quot;False&quot; 
            CellValuePresenterStyle=&quot;{StaticResource NameWithLinkCellStyle}&quot; 
            LabelMinWidth=&quot;100&quot; CellMinWidth=&quot;100&quot; 
        /> 
    </igDP:Field.Settings> 
</igDP:Field>

The Style applied to the CellValuePresenterStyle property is listed below:

XML
<Style x:Key=&quot;NameWithLinkCellStyle&quot; TargetType=&quot;{x:Type 
    igDP:CellValuePresenter}&quot;> 
    <Setter Property=&quot;Template&quot;> 
        <Setter.Value> 
             <ControlTemplate TargetType=&quot;{x:Type igDP:CellValuePresenter}&quot;> 
                 <TextBlock Margin=&quot;4,0,0,0&quot; VerticalAlignment=&quot;Center&quot;> 
                     <Hyperlink x:Name=&quot;hyperLink&quot; 
                         Command=&quot;view:Commands.ShowSalespersonDetails&quot; 
                         CommandParameter=&quot;{Binding Path=DataItem}&quot;  
                         Foreground=&quot;#333333&quot; 
                     >  
                         <TextBlock Text=&quot;{TemplateBinding Value}&quot; /> 
                     </Hyperlink> 
                 </TextBlock> 
                 <ControlTemplate.Triggers> 
                     <Trigger Property=&quot;IsMouseOver&quot; Value=&quot;True&quot;> 
                         <Setter 
                             Property=&quot;Foreground&quot; TargetName=&quot;hyperLink&quot; Value=&quot;black&quot; 
                         /> 
                     </Trigger> 
                 </ControlTemplate.Triggers> 
             </ControlTemplate> 
         </Setter.Value> 
    </Setter> 
</Style>

When the user clicks the Hyperlink in the cell, the custom ShowSalespersonDetails routed command executes. Since MainPage has a CommandBinding for that command, the following methods execute when that command is asked if it can execute and when it is executed. The execution logic is contained in the ShowSalesPersonDetailsChart method, which we examined previously. 

C#
void ShowSalespersonDetails_CanExecute(object sender, CanExecuteRoutedEventArgs e) 
{ 
    e.CanExecute = e.Parameter is SalesPersonViewModel; 
}

void ShowSalespersonDetails_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    SalesPersonViewModel salesPersonView = e.Parameter as SalesPersonViewModel; 
    this.ShowSalesPersonDetailsChart(salesPersonView); 
}

Learn more about the xamDataGrid. 

xamChart

xamSalesManager makes heavy use of xamChart. It displays various types of KPI data in charts, ranging from Sales By Territory to Salesperson Comparison data. The charts all use the “Neon” theme to achieve a visual style consistent with the rest of the UI. One of the more interesting uses of the xamChart in this program is the Salesperson Comparison feature. When the user checks a box in a salesperson’s row in the xamDataGrid, that salesperson’s sales performance data is added to the Salesperson Comparison chart. This feature allows for a quick assessment of multiple salespeople’s performance relative to each other.

The screenshot below shows this dynamic feature in action:

image004.jpg

Figure 4 - xamChart showing dynamic data selected from the xamDataGrid.

The logic behind this feature is in the SalesPersonComparisonControl.xaml.cs file. When that class detects that the user wants to add or remove a salesperson to/from the comparison chart, these methods execute:

C#
void AddSalesPerson(SalesPersonViewModel salesPerson) 
{ 
    Series series = new Series(); 
    series.Label = salesPerson.Name; 
    series.Marker = new Marker();

    foreach (var monthlySales in salesPerson.MonthlySalesHistoryUnfiltered) { 
        DataPoint dataPt = new DataPoint(); 
        dataPt.Label = monthlySales.DateDisplayText; 
        dataPt.Value = monthlySales.Actual; 
        dataPt.ToolTip = String.Format( 
            &quot;{0}{1}{2}{1}{3}&quot;, 
            salesPerson.Name, 
            Environment.NewLine, 
            monthlySales.DateDisplayText, 
            monthlySales.ActualDisplayText); 
        series.DataPoints.Add(dataPt); 
    }

    _salesPersonToSeriesMap[salesPerson] = series; 
    this.chart.Series.Add(series);

    if (this.chart.Visibility != Visibility.Visible) 
        this.chart.Visibility = Visibility.Visible;

    this.BringIntoView(); 
    ContentPane cp = LogicalTreeHelper.GetParent(this) as ContentPane; 
    if (cp != null) 
        cp.Activate();

    this.Focus(); 
}

void RemoveSalesPerson(SalesPersonViewModel salesPersonView) 
{ 
    if (!_salesPersonToSeriesMap.Keys.Contains(salesPersonView)) { 
        Debug.Fail(&quot;Not showing the specified salesperson.&quot;); 
        return; 
    }

    Series series = _salesPersonToSeriesMap[salesPersonView];

    _salesPersonToSeriesMap.Remove(salesPersonView); 
    this.chart.Series.Remove(series);

    if (this.chart.Series.Count == 0) 
    this.chart.Visibility = Visibility.Hidden; 
}

Learn more about the xamChart.

xamRibbon

An application such as xamSalesManager is bound to have many features and functionalities. Some of those features might only be available based on what the user is currently working on, and some might exist at all times. xamRibbon presents the application functionality in a compact and appealing manner, and offer certain features based on the current context of what the user is doing. In this sample application, most of the buttons in the xamRibbon do not perform any action, besides showing a message that explains the fact that they are non-functional. In a real application, of course, this would not be the case.

As seen in the following screenshot, the xamRibbon has a contextual tab, on the right. When a salesperson’s performance chart is active, that contextual tab will appear. If some other UI element is active, that tab disappears. In this screenshot, the performance chart of Denise McCort, a salesperson, is selected, which is why her name appears as a contextual tab.

image005.jpg

Figure 5 - xamRibbon exposing the xamSalesManager's rich feature set.

If the user were to select that contextual tab, they see with tools that with which to modify the associated salesperson’s chart. In the screenshot below, the user has reduced the number of months to display and removed the Actual Sales data from the chart. From the user’s perspective, this satisfies the desire to “View Denise McCort’s sales plan for the past six months.”

image006.jpg

Figure 6 - xamRibbon provides a contextual tab for customizing the selected salesperson's chart.

Learn more about the xamRibbon. 

Application Architecture

The xamSalesManager architecture borrows heavily from the DataModel-View-ViewModel pattern (also known as Model-View-ViewModel). MVVM is a very useful and popular pattern for creating WPF user interfaces, which somewhat resembles Martin Fowler’s Presentation Model pattern. The main difference between the two is that MVVM fills the gap between View and PresentationModel via data binding, as opposed to manually moving data between the two in code.

In case you are not already familiar with MVVM, here is a brief overview of how it works. Let’s start with a diagram:

image007.png

Figure 7 - The Model-View-ViewModel pattern.

The MVVM pattern is similar to classic Model View Presenter, only the Presenter morphs into a presentation-friendly set of data objects that adapt the data model to the demanding world of user interface controls. Those presentation-friendly data objects constitute the ViewModel.

Views bind directly to the ViewModel, and the state of all Views is stored in the ViewModel. In essence, the ViewModel is an abstraction of the application’s user interface, to which the UI controls are bound. All communication between various parts of the UI is indirect. Views react to changes made on the ViewModel objects by other Views, or by application logic. The important concept is that the “real” UI is the ViewModel, and the actual visual elements that happen to render it are almost arbitrary. Naturally, using the MVVM pattern does not mean user interfaces should be ugly! It simply means that the state and behavior of the UI should not reside in the UI itself, but the UI should be a “visual wrapper” around an abstraction of the UI (i.e. the ViewModel).

In xamSalesManager there are four DataModel classes, each with their corresponding ViewModel classes:

  • SalesRegion and SalesRegionViewModel
  • SalesTerritory and SalesTerritoryViewModel
  • SalesPerson and SalesPersonViewModel
  • MonthlySales and MonthlySalesViewModel

There is not a one-to-one relationship between Views and ViewModels. Any number of Views can display and update a ViewModel. For example, a SalesPersonViewModel object can be displayed in the DirectReportsControl (which contains the xamDataGrid), a SalesPersonControl, and the SalesPerson-ComparisonControl. This can be seen in the screenshot below, where the same salesperson, Chris Mangone, appears in three distinct UI Views at the same time.

image008.jpg

Figure 8 - Displaying the same ViewModel object for salesperson Chris Mangone in three Views simultaneously.

MainPage’s constructor loads the application’s data and ViewModel. The following method, in MainPage.xaml.cs, is responsible for instantiating the entire set of ViewModel objects:

C#
SalesRegionViewModel LoadViewModel() 
{ 
    // Get raw data about a sales region from the db. 
    SalesRegion salesRegion = Database.GetSalesRegion(SALES_REGION_ID);

    // Wrap that raw data into a set of presentation-friendly objects. 
    SalesRegionViewModel viewModel = new SalesRegionViewModel(salesRegion);

    // Let the UI bind to the ViewModel. 
    base.DataContext = viewModel;

    if (App.Current.Resources.Contains(&quot;DATA_SalesRegionViewModel&quot;)) 
    App.Current.Resources.Remove(&quot;DATA_SalesRegionViewModel&quot;);

    // Inject the viewmodel into the App's resource collection  
    // so that the Photo Field in the data grid can bind its  
    // Visibility property to the ShowEmployeePhotos property. 
    App.Current.Resources.Add(&quot;DATA_SalesRegionViewModel&quot;, base.DataContext);

    return viewModel; 
}

It might look like the LoadViewModel method creates only one ViewModel object, an instance of SalesRegionViewModel, but creating that instance kicks off a cascading series of ViewModel object initializations. The SalesRegionViewModel constructor shows how the entire ViewModel is constructed:

C#
public SalesRegionViewModel(SalesRegion salesRegion) 
{ 
    _salesRegion = salesRegion;

    _salesTerritories = new ReadOnlyCollection<SalesTerritoryViewModel>( 
        salesRegion.Territories 
        .Select(territory => new SalesTerritoryViewModel(territory, this)) 
        .ToList() 
    );

    _salesPeople = new ReadOnlyCollection<SalesPersonViewModel>( 
        salesRegion.SalesPeople 
        .Select(salesPerson => new SalesPersonViewModel( 
        salesPerson, 
        _salesTerritories.FirstOrDefault( 
        t => t.ID == salesPerson.TerritoryID))) 
        .ToList() 
    );

    List<MonthlySalesViewModel> history = 
        salesRegion.MonthlySalesHistory 
        .Take(6) 
        .ToList()  
        .ConvertAll(ms => new MonthlySalesViewModel(ms));

    history.Sort(); 
    _monthlySalesHistory = new ReadOnlyCollection<MonthlySalesViewModel>(history);

    _salesPeopleBeingCompared = new ObservableCollection<SalesPersonViewModel>();

    double maxTotalSales = 0.0; 
    foreach (SalesPersonViewModel salesPerson in this.SalesPeople) 
    { 
        double totalSales = salesPerson.MonthlySalesHistory.Sum(ms => ms.Actual); 
        if (maxTotalSales < totalSales) 
        { 
            maxTotalSales = totalSales; 
            _topPerformingSalesPerson = salesPerson; 
        } 
    } 
}

If you are interested in learning more about how the ViewModel classes work, they are all contained in a folder named ViewModel within the xamShowcase’s Visual Studio® 2008 project. In general, they are simply wrapper classes around a data object, and typically implement INotifyPropertyChanged so that the WPF binding system can monitor settable properties for new values. They also expose UI-specific properties, such as whether or not to show photos in the xamDataGrid; thus serving as a well-organized backing store for the UI.

Conclusion

xamSalesManager is an executive dashboard that targets the Regional Sales Manager of a company. It makes use of several UI components in NetAdvantage for WPF to create a rich, compelling user interface. The flexibility added by the xamDockManager adds significant value to the UI because it allows the user to open and position as many performance charts as desired. High-level financial aggregations are displayed in the xamDataGrid as row summaries. The xamRibbon provides a compact means of presenting a large amount of application functionality, and even supports contextual tabs that exist when a salesperson’s performance chart is active.

Coupling all of the powerful UI features of NetAdvantage for WPF with the DataModel-View-ViewModel pattern establishes a firm foundation on top of which a full-fledged application could grow. MVVM takes advantage of WPF’s rich support for data binding, allowing you to leverage the full power of the Infragistics UI components with simple data binding and minimal amounts of code.

Try the xamSalesManager for yourself and if you haven’t already you should download the latest version of the NetAdvantage for WPF bundle to get all of its WPF controls, documentation and the source code for xamSalesManager and many other samples.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions