65.9K
CodeProject 正在变化。 阅读更多。
Home

深入了解 xamSalesManager:WPF 的 NetAdvantage 演示样本

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.74/5 (14投票s)

2008年7月22日

CPOL

11分钟阅读

viewsIcon

55584

Infragistics 深入探讨了其 WPF xamSalesManager 展示样本的技术架构。了解如何将 Model - View - ViewModel 模式应用于您自己的应用程序设计,以创建引人注目的用户体验,就像这个复杂的销售仪表板一样。

目录

引言

本文档介绍了 xamSalesManager,这是 Infragistics 发布的一款 WPF 展示样本,作为 NetAdvantage for WPF 2008 Vol. 1 的一部分。xamSalesManager 托管在 Infragistics xamShowcase 应用程序中,该应用程序包含在 NetAdvantage for WPF 安装中,并且可以通过 Web 作为 XBAP 运行。

什么是展示样本?

Infragistics 在其每款产品中都包含两种类型的样本:功能样本和展示样本。功能样本展示控件的特定功能,例如启用数据网格的行汇总。展示样本则在真实应用程序的上下文中展示多个控件协同工作,例如区域销售经理的仪表板。

展示样本的目标

截至 NetAdvantage for WPF 2008 Vol. 1,展示样本已承担提供架构指导的新角色,同时展示 Infragistics WPF 控件的样式和性能能力。从 xamSalesManager 开始,每个新的展示样本都演示了在业务应用程序中使用 Infragistics WPF 组件套件的标准和创新方法。此外,展示样本符合行业最佳实践,并采用成熟的设计和用户体验 (UX) 模式。

展示样本并非功能齐全的应用程序,而是仅提供足够的功能来演示所呈现的组件、UI 模式和交互设计。任何非功能性的 UI 元素在您与之交互时都会显示一条消息,以避免混淆。

最终目标是演示如何利用 NetAdvantage for WPF 在任何 WPF 应用程序中创建出色的用户体验。

什么是 xamSalesManager?

xamSalesManager 是一款为虚构公司的区域销售经理设计的执行仪表板应用程序。它提供关于其销售区域内各个区域以及每位销售人员的高级别报告信息。该应用程序以表格布局(即数据网格)和图表形式显示关键绩效指标 (KPI) 信息。它允许用户在同一图表中比较任何销售人员的销售绩效指标。它还能够查看数据网格中的汇总绩效摘要。

下面是加载 xamSalesManager 后的截图

image001.jpg

图 1 - xamSalesManager 启动时会展示公司销售人员的信息。

UI 架构

Infragistics 非常注重确保 NetAdvantage for WPF 组件能够轻松无缝地协同工作。这保证了我们可以创建丰富的用户界面,实现一致的视觉风格,并轻松地集成 Microsoft 的标准控件。本文档的这一部分将回顾 xamSalesManager 界面中使用的 UI 组件,并展示它们如何组合以创建吸引人的用户体验。xamSalesManager 使用了以下四个 NetAdvantage for WPF 组件:

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

接下来的几节将回顾 UI 如何使用这些组件。

xamDockManager

加载应用程序时,它会显示四个 xamCharts。用户可以随时打开并查看任意数量的销售人员绩效图表。该应用程序依赖 xamDockManager 来组织所有这些 xamCharts。每个 xamChart 都存在于 xamDockManager 的单独窗格中,使用户可以完全自由地决定图表的位置和大小。

下面显示的 xamDockManager 已应用于其窗格的“Office2k7Black”主题

image002.jpg

图 2 – xamDockManager 托管了多个 xamCharts。

应用程序的代码中很少会直接操作 xamDockManager。在大多数情况下,您可以完全通过 XAML 来使用该组件,而无需编写一行代码。xamSalesManager 应用程序动态创建停靠窗格,这是一项在代码中执行的任务。以下是 `MainPage.xaml.cs` 中负责该任务的方法,当用户单击 xamDataGrid 中销售人员姓名时会执行此方法:

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(); 
}

该方法会查找或创建一个 `SalesPersonControl` 以在 `ContentPane` 中显示。然后,它会调用窗格上的 `BringIntoView` 以确保它在 UI 中可见。之后,它会向上遍历逻辑树以查找承载该控件的 `ContentPane`。它会调用窗格上的 `Activate` 以确保它成为 xamDockManager 的 `ActivePane`。`ActivePane` 中的销售人员会在 xamRibbon 中获得一个上下文选项卡,我们稍后将对此进行探讨。

了解更多关于 xamDockManager 的信息. 

xamDataGrid

该应用程序在 xamDataGrid 中显示其销售人员列表。该网格通过行汇总提供汇总的财务数据。默认情况下,“年度累计销售额”(Year-to-Date Sales)字段会显示销售区域内所有销售人员的累计销售总额。用户可以通过单击该字段的标题中的行汇总下拉菜单,并从可用汇总列表中选择更多选项来添加更多汇总。单击销售人员姓名将打开其销售绩效图表。勾选销售人员行中第一个单元格的复选框会将该人员添加到销售人员比较图表中。

image003.jpg

图 3 – xamDataGrid 按区域显示销售人员,并提供销售总额的行汇总。

在上一个屏幕截图中,您会注意到所有销售人员都按其“区域”字段进行了分组。在分组标题行中,您可以看到该区域的总销售额。xamDataGrid 底部的行汇总显示了整个销售区域的总销售额。

“销售人员姓名”字段中的单元格显示一个超链接,单击该超链接将打开相应的销售人员的绩效图表。这是 `DirectReportsControl.xaml` 文件中该字段的声明:

<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>

应用于 `CellValuePresenterStyle` 属性的 `Style` 如下所示:

<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>

当用户单击单元格中的 `Hyperlink` 时,自定义的 `ShowSalespersonDetails` 路由命令会执行。由于 `MainPage` 对该命令具有 `CommandBinding`,因此在命令被询问是否可以执行以及执行时,将执行以下方法。执行逻辑包含在 `ShowSalesPersonDetailsChart` 方法中,我们之前已经对此进行了检查。

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); 
}

了解更多关于 xamDataGrid 的信息. 

xamChart

xamSalesManager 大量使用了 xamChart。它以图表形式显示各种 KPI 数据,从区域销售额到销售人员比较数据。这些图表都使用“Neon”主题,以实现与 UI 其他部分一致的视觉风格。xamChart 在此程序中最有趣的用法之一是销售人员比较功能。当用户勾选 xamDataGrid 中销售人员行中的复选框时,该销售人员的销售绩效数据将被添加到销售人员比较图表中。此功能可以快速评估多个销售人员的相对绩效。

下图展示了此动态功能的实际应用:

image004.jpg

图 4 - xamChart 显示从 xamDataGrid 中动态选择的数据。

此功能背后的逻辑位于 `SalesPersonComparisonControl.xaml.cs` 文件中。当该类检测到用户想要将某个销售人员添加到比较图表或从中移除时,将执行这些方法:

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; 
}

了解更多关于 xamChart 的信息.

xamRibbon

像 xamSalesManager 这样的应用程序必然有很多功能。其中一些功能可能仅基于用户当前正在处理的内容而可用,而另一些则可能始终存在。xamRibbon 以简洁而吸引人的方式展示应用程序的功能,并根据用户当前的操作上下文提供某些功能。在此示例应用程序中,xamRibbon 中的大多数按钮除了显示解释其非功能性的消息外,不执行任何操作。当然,在实际应用程序中情况并非如此。

如下图所示,xamRibbon 右侧有一个上下文选项卡。当销售人员的绩效图表处于活动状态时,该上下文选项卡将显示。如果其他 UI 元素处于活动状态,则该选项卡会消失。在此屏幕截图中,销售人员 Denise McCort 的绩效图表被选中,这就是为什么她的名字会以上下文选项卡的形式出现。

image005.jpg

图 5 - xamRibbon 暴露了 xamSalesManager 丰富的功能集。

如果用户单击该上下文选项卡,他们会看到用于修改相关销售人员图表的工具。在下图中,用户减少了要显示的月份数,并从图表中删除了实际销售数据。从用户的角度来看,这满足了“查看 Denise McCort 过去六个月的销售计划”的愿望。

image006.jpg

图 6 - xamRibbon 为自定义选定的销售人员图表提供上下文选项卡。

了解更多关于 xamRibbon 的信息. 

应用程序架构

xamSalesManager 的架构在很大程度上借鉴了 DataModel-View-ViewModel 模式(也称为 Model-View-ViewModel)。MVVM 是创建 WPF 用户界面的非常有用的流行模式,它在某种程度上类似于 Martin Fowler 的 Presentation Model 模式。两者之间的主要区别在于,MVVM 通过数据绑定填补了 View 和 PresentationModel 之间的空白,而不是在代码中手动移动数据。

如果您还不熟悉 MVVM,这里是其工作原理的简要概述。我们从一个图表开始:

image007.png

图 7 - Model-View-ViewModel 模式。

MVVM 模式类似于经典的 Model View Presenter,只是 Presenter 演变成了一组面向演示的、适应数据模型的、适合用户界面控件需求的友好数据对象。这些面向演示的友好数据对象构成了 ViewModel。

Views 直接绑定到 ViewModel,并且所有 Views 的状态都存储在 ViewModel 中。本质上,ViewModel 是应用程序用户界面的抽象,UI 控件绑定到它。UI 各部分之间的所有通信都是间接的。Views 会响应由其他 Views 或应用程序逻辑对 ViewModel 对象所做的更改。重要的概念是,“真实” UI 是 ViewModel,而碰巧渲染它的实际视觉元素几乎是任意的。当然,使用 MVVM 模式并不意味着用户界面应该很丑陋!它仅仅意味着 UI 的状态和行为不应该存在于 UI 本身中,而 UI 应该是一个“视觉包装器”,围绕着 UI 的抽象(即 ViewModel)。

在 xamSalesManager 中,有四个 DataModel 类,分别对应着它们的 ViewModel 类:

  • `SalesRegion` 和 `SalesRegionViewModel`
  • `SalesTerritory` 和 `SalesTerritoryViewModel`
  • `SalesPerson` 和 `SalesPersonViewModel`
  • `MonthlySales` 和 `MonthlySalesViewModel`

Views 和 ViewModels 之间不存在一对一的关系。任何数量的 Views 都可以显示和更新一个 ViewModel。例如,一个 `SalesPersonViewModel` 对象可以显示在 `DirectReportsControl`(包含 xamDataGrid)中、`SalesPersonControl` 中以及 `SalesPerson-ComparisonControl` 中。如下图所示,同一销售人员 Chris Mangone 同时出现在三个不同的 UI Views 中。

image008.jpg

图 8 - 同时在三个 Views 中显示销售人员 Chris Mangone 的同一个 ViewModel 对象。

`MainPage` 的构造函数会加载应用程序的数据和 ViewModel。以下 `MainPage.xaml.cs` 中的方法负责实例化整个 ViewModel 对象集:

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; 
}

看起来 `LoadViewModel` 方法只创建了一个 ViewModel 对象,即 `SalesRegionViewModel` 的一个实例,但创建该实例会启动一系列级联的 ViewModel 对象初始化。`SalesRegionViewModel` 构造函数展示了整个 ViewModel 是如何构建的:

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; 
        } 
    } 
}

如果您有兴趣了解更多关于 ViewModel 类如何工作的信息,它们都包含在 xamShowcase 的 Visual Studio® 2008 项目的 ViewModel 文件夹中。总的来说,它们只是数据对象的包装类,通常实现 `INotifyPropertyChanged`,以便 WPF 绑定系统可以监视可设置的属性以获取新值。它们还公开 UI 特定的属性,例如是否要在 xamDataGrid 中显示照片;因此,它们是 UI 的一个组织良好的后备存储。

结论

xamSalesManager 是一款针对公司区域销售经理的执行仪表板。它利用 NetAdvantage for WPF 的多个 UI 组件来创建丰富、引人注目的用户界面。xamDockManager 增加的灵活性为 UI 带来了显著的价值,因为它允许用户打开并放置任意数量的绩效图表。高层财务汇总在 xamDataGrid 中显示为行汇总。xamRibbon 提供了一种简洁的方式来呈现大量的应用程序功能,甚至支持在销售人员绩效图表处于活动状态时出现的上下文选项卡。

将 NetAdvantage for WPF 强大的 UI 功能与 DataModel-View-ViewModel 模式相结合,为构建功能齐全的应用程序奠定了坚实的基础。MVVM 利用 WPF 对数据绑定的丰富支持,使您能够通过简单的数据绑定和最少量的代码来充分发挥 Infragistics UI 组件的强大功能。

亲自试用 xamSalesManager,如果您还没有下载过,那么您应该下载最新版本的 NetAdvantage for WPF 套件,以获取其所有 WPF 控件、文档以及 xamSalesManager 和许多其他示例的源代码。

© . All rights reserved.