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

为 X11 编写 XAML 功能区应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2014年10月29日

CPOL

18分钟阅读

viewsIcon

25299

downloadIcon

749

目前,主要的 Linux/Unix (X11) GUI 应用程序框架 (GTK+, KDE) 都不支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)于2012年5月29日被废弃。本文介绍了如何在 C# 中使用 Roma Widget Set (Xrw) 创建基于 XAML 的 Ribbon 应用程序。

引言

本文是一个实践教程,介绍了如何使用 Roma Widget Set (Xrw) 在 C# 中编写基于 MVVM(模型-视图-视图模型)设计模式的 X11 ribbon 命令界面应用程序(一个稍微复杂一点的 GUI)。Roma Widget Set 是一个零依赖的 X11 GUI 应用程序框架(它只需要免费 Mono 标准安装的程序集和免费 X11 发行版的库;它不需要 GNOME、KDE 或商业库),并且完全用 C# 实现。

本文延续了 为 X11 编写 XAML 对话框应用程序的工作。据我所知,这是在 Moonlight 被废弃后,首次尝试将 XAML 用于 X11 应用程序开发(利用 Xrw)。

Roma Widget Set 和 XAML 实现都不完整。这个示例应用程序旨在作为一个更复杂的“概念验证”,检查是否以及如何使用 XAML 创建基于 MVVM 设计模式的 X11 应用程序。

由于第二次尝试将 XAML 用于 X11 应用程序开发已经成功,因此接下来肯定会有更多关于使用 Roma Widget Set 在 X11 上使用 XAML 的文章。

背景

使用 XAML 进行 X11 应用程序开发的动机和通用概念已在为 X11 编写 XAML 对话框应用程序一文中解释。

焦点

第一篇文章演示了 XAML 可以实现:

  • 定义一个包含一些控件的窗口,并且
  • 将点击事件连接到按钮上,

本文将演示 XAML 可以实现:

  • 定义一个包含 Ribbon 命令界面的窗口,
  • 定义静态窗口资源(本示例有一个资源转换器和一个 ModelView),
  • 将 ModelView 作为静态资源分配给控件的数据上下文,
  • 将资源转换器作为静态资源应用于控件的属性,
  • 通过“RelayCommand”方法将命令绑定到按钮,
  • 将控件属性绑定到数据上下文,并且
  • 通过 `INotifyPropertyChanged` 接口更新控件。

使用代码

示例应用程序使用 Mono Develop 2.4.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境上为 Mono 2.8.1 编写。移植到任何更旧或更新的版本都应该没有问题。示例应用程序的解决方案包含两个项目(完整的源代码可供下载)

  • XamlRibbonApp 包含示例应用程序的源代码。
  • XamlPreprocessor 包含 XAML 预处理器的源代码。

该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。

32 位和 64 位解决方案之间唯一的区别是一些 X11 特定数据类型的定义,这在 使用 Mono Develop 编程 Xlib - 第一部分:低级 (概念验证) 一文中已经描述过。

Xlib/X11 窗口处理基于 X11Wrapper 程序集版本 0.7,它定义了 libX11.so 的 Xlib/X11 调用函数原型、结构和类型。该程序集是为 使用 Mono Develop 编程 Xlib - 第一部分:低级 (概念验证) 项目开发的,并在 编程 Roma Widget Set (C# X11) - 零依赖 GUI 应用程序框架 - 基础 项目期间得到改进。

GUI 框架基于 Xrw 程序集版本 0.7,它定义了 XAML 代码中使用的部件/小部件及其包装类(应尽可能接近 Microsoft® 原版)。该程序集是在 编程 Roma Widget Set (C# X11) - 零依赖 GUI 应用程序框架 - 基础 项目期间开发的。

建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。

该图片展示了使用 `XrwTheme.GeneralStyle.Gtk2Clearlooks` 的示例应用程序。

示例应用程序(通过 Ribbon 的应用程序菜单项“打开...”)打开并逐行显示一个 CSV 文件(SampleData.csv 已包含在示例应用程序的源代码中)。

所有 `RibbonApplicationMenuItem` 和所有 `RibbonButton` 都连接到 `RelayCommand`,其中大部分仅用于演示目的,显示一个消息框。与连接到必须包含在视图代码隐藏文件中的事件委托的 `Click` 属性不同,`Command` 属性连接到必须包含在当前 `DataContext` 代码文件(通常是 ViewModel)中的命令类实例。这导致了更好的 MVVM 模式实现,没有代码隐藏(在 CodeProject 上搜索“RelayCommand”或参阅“在 XAML 中使用 EventTrigger 实现 MVVM – 无代码隐藏”以了解为何应避免代码隐藏)。

所有 `RibbonApplicationMenuItem` 和所有 `RibbonButton` 的图像均取自框架资源文件 (xmlns:properties="clr-namespace:X11.Properties"),并通过 `BitmapResourceToImageConverter` 类转换为图像资源。

Windows 版本,与 XAML 代码并行开发,旨在尽可能接近 Microsoft® 原版,它从应用程序资源文件 (xmlns:properties="clr-namespace:XamlRibbonApp.Properties") 获取图像,并使用 `BitmapResourceToImageConverter` 类的不同实现将图像转换为图像资源。这种方法完全隐藏了 X11/Windows® 平台的特定细节。

逐步演示

项目设置应用程序文件上下文(主题除外)和预处理器代码生成步骤与为 X11 编写 XAML 对话框应用程序中的完全相同。如果需要从头开始创建新解决方案,请参阅该文章。

主视图文件上下文

XAML (MainView.xaml)

首先是省略了 Ribbon 定义的 XAML 文件。

<RibbonWindow   x:Class="XamlRibbonApp.MainView"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:src="clr-namespace:XamlRibbonApp"
                xmlns:properties="clr-namespace:X11.Properties"
                Name="MainWindow" Title="XAML Tester" Height="600" Width="500"
                MinWidth="300" MinHeight="250" Icon="XrwIcon16.bmp">
    <Window.Resources>
        <src:BitmapResourceToImageConverter x:Key="ImageConverter" />
        <src:MainWindowViewModel x:Key="MainViewModel" />
    </Window.Resources>
    <Grid Name="MainGrid" DataContext="{StaticResource MainViewModel}">
        <Grid.Resources>
            <!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
        </Grid.Resources>
        <Grid.Datacontext>
            <!-- <Binding Source="{StaticResource mainViewDataSource}"/> -->
        </Grid.Datacontext>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <!-- The fixed height of 117 is the required heigth of a ribbon.  -->
            <!-- If a margin is set, the margin must be added to the height. -->
            <RowDefinition Height="117"/>
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="28"/>
        </Grid.RowDefinitions>

        <!-- ATTENTION: (Microsoft compatibility issue) The ribbon out of the System.Windows.Controls. -->
        <!--            Ribbon.dll needs a System.Windows.Controls.RibbonWindow as topmost container. -->
        <!-- OTHERWISE: A "System.Windows.Data Error: 4 : Cannot find source for binding with -->
        <!--            reference 'RelativeSource FindAncestor, AncestorType='Microsoft.Windows.  -->
        <!--            Controls.Ribbon.RibbonWindow', ... " occures. -->
        <!-- APPARENTLY: The Microsoft.Windows.Controls.Ribbon namespace (Office 2010 ribbon control -->
        <!--             from October 2010) is replaced by System.Windows.Controls.Ribbon namespace -->
        <!--             (WPF ribbon control from .NET 4.5). -->
        <Ribbon Name="MainRibbon" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="0">
            ...
        </Ribbon>
        
        <ListView Name="FileContentListView" Grid.Row="1" ItemsSource="{Binding DocumentRows}" >
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Row" Width="40" DisplayMemberBinding="{Binding Row}"  />
                        <GridViewColumn Header="Content" DisplayMemberBinding="{Binding Path=Content}"  />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
        <Label  Name="StateLine" Content="{Binding CurrentFilePath}" Grid.Column="0" Grid.Row="2"
                BorderThickness="1" />
    </Grid>
</RibbonWindow>

完整的 XAML 代码与 Microsoft® 完全兼容。

为 X11 编写 XAML 对话框应用程序 相比,本文将只讨论增强功能和差异。

对于最顶层的容器控件,使用 `RibbonWindow` 而不是 `Window`。这是为了防止出现“System.Windows.Data Error: 4 : 无法找到绑定源,引用 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.Ribbon.RibbonWindow', AncestorLevel='1''. BindingExpression:Path=WindowState; DataItem=null; 目标元素是 'Ribbon' (Name='MainRibbon'); 目标属性是 'NoTarget' (type 'Object')”和“System.Windows.Data Error: 4 : 无法找到绑定源,引用 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.Ribbon.RibbonWindow', AncestorLevel='1''. BindingExpression:Path=IsActive; DataItem=null; 目标元素是 'Ribbon' (Name='MainRibbon'); 目标属性是 'NoTarget' (type 'Object')”错误。

  • xmlns:properties 属性定义本地资源属性的命名空间。(Microsoft .NET 项目包含 Resources.resx 文件以保存语言相关的资源,如字符串和图像/图标。Xrw 通过简单地使用 C# 文件来模拟此技术。)如果任何属性值使用前缀 properties: 作为引用,则建议使用或强制使用此属性。属性值的语法必须是 clr-namespace:<命名空间名称>。以这种方式定义的命名空间可以通过属性值通过前缀 properties: 引用。
  • MinWidth 属性定义了最小宽度限制。此属性是可选的。正整数被解释为像素宽度。Microsoft® 原版 WPF Ribbon 控件(.NET 4.5)在主窗口宽度小于 295 时会消失。因此,MinWidth 设置为 300。
  • `MinHeight` 属性定义了最小高度限制。此属性是可选的。正整数被解释为像素高度。
  • Window.Resources 节点现在被评估,并定义与 Window 类实例相关的静态资源。此节点是可选的。节点的语法必须是<命名空间名称>:<类名> x:Key="<别名>"。此处定义的静态资源在窗口类实例化期间创建,并与窗口类实例的生命周期绑定。由于静态资源的搜索算法(遍历控件层次结构),窗口类实例及其所有子控件实例可以通过其<别名>引用此处定义的资源。

定义与窗口类实例关联的静态资源很有用,因为它们可以被多次引用(但只需加载到内存中一次),并且可以被所有子控件实例引用。如果应用程序支持多个窗口,或者静态资源应该与应用程序类实例的生命周期绑定,而不是与单个窗口类实例的生命周期绑定,那么在 `App.xaml` 的 `Application.Resources` 节点中定义应用程序范围的静态资源可能会更好,该节点现在也已评估。在这种情况下,由于静态资源的搜索算法,所有窗口类实例及其所有子控件实例都可以通过其<别名>引用此处定义的资源。

Grid 是窗口的根布局管理器小部件。在 为 X11 编写 XAML 对话框应用程序 中已经讨论过的 Grid 用法没有增强。

Ribbon 将稍后讨论。

ListView 将通过以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Grid.Column 属性定义了控件在网格中定位的基于零的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 列的控件是强制性的。索引不得超过可用网格行。
  • Grid.Row 属性定义了控件在网格中定位的基于零的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 行的控件是强制性的。索引不得超过可用网格行。
  • ItemsSource 属性定义了控件获取要显示项的数据源。数据源必须实现 System.Collections.IEnumerable此属性通常是可选的,但要绑定数据以显示则必需。
  • `ListView.View` 节点提供对子对象的访问,该子对象定义了数据在 `ListView` 中如何样式化和组织。此节点是强制性的。它必须包含一个定义子控件的节点。目前仅支持 `GridView` 节点作为子节点。

GridView 将由以下内容定义

  • GridView.Columns 节点连接列定义。此节点是强制性的。
    - `GridColumn` 节点定义一个网格视图列。此节点是强制性的,至少一次。
       - `Header` 属性定义列标题显示文本。此属性是可选的。
       - Width 属性定义了网格列的宽度。此属性是可选的。正整数被解释为像素宽度,并覆盖任何自动宽度计算。省略 width 属性将导致自动宽度计算。
       - `DisplayMemberBinding` 属性将任何指定的 `ItemsSource` 元素的属性名与要在此列中显示的值相关联。此属性通常是可选的,但显示值时是必需的。

现在回到 `Ribbon`。这是带有 ribbon 定义的 XAML 文件摘录。(第一个 ribbon 选项卡的第二个 ribbon 组和第二个 ribbon 选项卡被省略了,它们使用了与第一个 ribbon 选项卡的第一个组相同的技术。)

<Ribbon Name="MainRibbon" Grid.Column="0" Grid.Row="0">
    <Ribbon.ApplicationMenu>
        <RibbonApplicationMenu>
            <RibbonApplicationMenuItem Name="AppMenuItem0" Header="New"
                Command="{Binding RibbonAppMenu_FileNew_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_NEW_32_WIN},
                              Converter={StaticResource ImageConverter}}" />
            <RibbonApplicationMenuItem Name="AppMenuItem1" Header="Open ..."
                Command="{Binding RibbonAppMenu_FileOpen_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_OPEN_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
            <RibbonSeparator />
            <RibbonApplicationMenuItem Name="AppMenuItem2" Header="Save"
                Command="{Binding RibbonAppMenu_FileSave_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_SAVE_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
            <RibbonApplicationMenuItem Name="AppMenuItem3" Header="Save as ..."
                Command="{Binding RibbonAppMenu_FileSaveAs_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_SAVEAS_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
            <RibbonSeparator />
            <RibbonApplicationMenuItem Name="AppMenuItem4" Header="Options"
                Command="{Binding RibbonAppMenu_Options_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_OPTIONS_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
            <RibbonApplicationMenuItem Name="AppMenuItem5" Header="Close"
                Command="{Binding RibbonAppMenu_Close_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_CLOSE_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
            <RibbonSeparator />
            <RibbonApplicationMenuItem Name="AppMenuItem6" Header="Exit"
                Command="{Binding RibbonAppMenu_Exit_Command}" CommandParameter="null"
                ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_EXIT_32_WIN},
                                              Converter={StaticResource ImageConverter}}" />
        </RibbonApplicationMenu>
    </Ribbon.ApplicationMenu>
    <RibbonTab Name="RibbonTabEdit" Header="Edit">
        <RibbonGroup Name="RibbonGroupClipboard" Header="Clipboard" >
            <RibbonButton Name="RibbonButtonClipboardCut" Label="Cut"
               Command="{Binding RibbonTabEdit_ClipboardCut_Command}"
               LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_CUT_32_WIN},
                                                  Converter={StaticResource ImageConverter}}"
               SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_CUT_16_WIN},
                                                  Converter={StaticResource ImageConverter}}" />
            <RibbonButton Name="RibbonButtonClipboardCopy" Label="Copy"
                Command="{Binding RibbonTabEdit_ClipboardCopy_Command}"
                LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_COPY_32_WIN},
                                                   Converter={StaticResource ImageConverter}}"
                SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_COPY_16_WIN},
                                                   Converter={StaticResource ImageConverter}}" />
            <RibbonButton Name="RibbonButtonClipboardPaste" Label="Paste"
                Command="{Binding RibbonTabEdit_ClipboardPaste_Command}"
                LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_PASTE_32_WIN},
                                                   Converter={StaticResource ImageConverter}}"
                SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_PASTE_16_WIN},
                                                   Converter={StaticResource ImageConverter}}" />
        </RibbonGroup>
        <RibbonGroup Name="RibbonGroupOrder" Header="Order">
            ...
        </RibbonGroup>
    </RibbonTab>
    <RibbonTab Name="RibbonTabView" Header="View">
        ...
    </RibbonTab>
    <RibbonTab Name="RibbonTabHelp" Header="Help">
        <RibbonGroup Name="RibbonGroupHelp" Header="Help">
            <RibbonSplitButton Name="RibbonButtonHelp" Label="Help"
                LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_32},
                                                   Converter={StaticResource ImageConverter}}"
                SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
                                                   Converter={StaticResource ImageConverter}}" >
                <RibbonMenuItem Header="Local Help"
                    Command="{Binding RibbonTabHelp_LocalHelp_Command}"
                    ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
                                                  Converter={StaticResource ImageConverter}}"/>
                <RibbonMenuItem Header="Online Help"
                    Command="{Binding RibbonTabHelp_OnlineHelp_Command}"
                    ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
                                                  Converter={StaticResource ImageConverter}}"/>
            </RibbonSplitButton>
            <RibbonButton Name="RibbonButtonAbout" Label="About"
                    Command="{Binding RibbonTabHelp_About_Command}"
                    LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_INFO_32},
                                                       Converter={StaticResource ImageConverter}}"
                    SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_INFO_16},
                                                       Converter={StaticResource ImageConverter}}" />
        </RibbonGroup>
    </RibbonTab>
</Ribbon>

完整的 XAML 代码与 Microsoft® 完全兼容。

Ribbon 将通过以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Grid.Column 属性定义了控件在网格中定位的基于零的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 列的控件是强制性的。索引不得超过可用网格行。
  • Grid.Row 属性定义了控件在网格中定位的基于零的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 行的控件是强制性的。索引不得超过可用网格行。
  • `Ribbon.ApplicationMenu` 节点提供对子对象的访问,该子对象定义应用程序菜单。此节点通常是可选的,但要显示应用程序菜单则必需。它必须包含一个定义应用程序菜单的节点。目前仅支持 `RibbonApplicationMenu` 节点作为子节点。
    - `RibbonApplicationMenu` 节点定义应用程序菜单。此节点是强制性的。它必须包含定义菜单项的节点。
  • RibbonTab 节点定义一个 Ribbon 选项卡,并包含定义子控件的节点。目前只支持 RibbonGroup 节点作为子节点。

RibbonApplicationMenu 控件包含 `RibbonApplicationMenuItem` 控件作为子控件,它们将按以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Header 属性定义要显示的文本。此属性是可选的。
  • Command 属性定义要执行的“RelayCommand”。此属性是可选的。Command 相较于 Click 的优点是,操作代码可以位于 ModelView 代码中,而不是视图的代码隐藏中。
  • ImageSource 属性定义要显示的图标。此属性是可选的。属性的语法必须是<相对图像文件URI>{Binding Source={x:Static <命名空间名称>:<资源名称>} Converter={StaticResource <转换器类别名>}} 用于本地静态资源,以及{Binding Source={StaticResource <资源别名>} Converter={StaticResource <转换器类别名>}} 用于可引用的静态资源。Converter 是可选的。

{Binding ... } 语法提供了使用预编译资源而不是图像文件的可能性,因此也避免了在程序集之外提供大量图像文件。

RibbonApplicationMenu 控件包含 RibbonSeparator 控件作为子控件。Microsoft® 实现将完全独立处理此节点,而 X11/Xrw 实现会修改先前的 RibbonApplicationMenuItem 以显示分隔符。因此,分隔符不能在没有先前的 RibbonApplicationMenuItem 的情况下出现,也不能立即连续出现。

RibbonTab 控件包含 `RibbonGroup` 控件作为子控件,它们将按以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Header 属性定义要显示的文本。此属性是可选的。
  • RibbonButton 节点定义一个 Ribbon 按钮作为子控件。此节点是可选的。
  • RibbonSplitButton 节点定义一个 Ribbon 拆分按钮作为子控件。此节点是可选的。

`RibbonGroup` 控件包含 `RibbonButton` 控件作为子控件,它们将按以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Label 属性定义要显示的文本。此属性是可选的。
  • Command 属性定义要执行的“RelayCommand”。此属性是可选的。Command 相较于 Click 的优点是,操作代码位于 ModelView 代码中,而不是视图的代码隐藏中。
  • LargeImageSource 属性定义了用于尺寸定义 ImageSize="Large" 的大图标,而 SmallImageSource 属性定义了用于尺寸定义 ImageSize="Small" 的小图标。这些属性是可选的。属性的语法必须是<相对图像文件 URI>{Binding Source={x:Static <命名空间名称>:<资源名称>} Converter={StaticResource <转换器类别名>}} 用于此节点本地的静态资源,以及{Binding Source={StaticResource <资源别名>} Converter={StaticResource <转换器类别名>}} 用于可引用的静态资源。Converter 是可选的。

`RibbonGroup` 控件包含 `RibbonSplitButton` 控件作为子控件,它们将按以下方式定义

  • Name 属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。
  • Label 属性定义要显示的文本。此属性是可选的。
  • LargeImageSource 属性定义了用于尺寸定义 ImageSize="Large" 的大图标,而 SmallImageSource 属性定义了用于尺寸定义 ImageSize="Small" 的小图标。这些属性是可选的。属性的语法必须是<相对图像文件 URI>{Binding Source={x:Static <命名空间名称>:<资源名称>} Converter={StaticResource <转换器类别名>}} 用于此节点本地的静态资源,以及{Binding Source={StaticResource <资源别名>}} 用于可引用的静态资源。
  • RibbonMenuItem 节点定义了 Ribbon 拆分按钮的一个菜单项。此节点是强制性的,至少一次。目前只支持 RibbonMenuItem 节点作为子节点。
    - Header 属性定义要显示的文本。此属性是可选的。
    - Command 属性定义要执行的“RelayCommand”。此属性是可选的。Command 相较于 Click 的优点是,操作代码位于 ModelView 代码中,而不是视图的代码隐藏中。
    - ImageSource 属性定义要显示的图标。此属性是可选的。属性的语法必须是<相对图像文件URI>{Binding Source={x:Static <命名空间名称>:<资源名称>} Converter={StaticResource <转换器类别名>}} 用于本地静态资源,以及{Binding Source={StaticResource <资源别名>} Converter={StaticResource <转换器类别名>}} 用于可引用的静态资源。Converter 是可选的。

如果为 `RibbonButton` 或 `RibbonSplitButton` 定义了 `LargeImageSource` 属性,则默认情况下会以 `ImageSize="Large"` 显示(无论是否定义了 `SmallImageSource` 属性)。

如果省略了 `LargeImageSource` 属性,并且为 `RibbonButton` 或 `RibbonSplitButton` 定义了 `SmallImageSource` 属性,则默认情况下会以 `ImageSize="Small"` 和 `IsLabelVisible="True"` 显示。

如果 `RibbonButton` 或 `RibbonSplitButton` 既没有定义 `LargeImageSource` 属性,也没有定义 `SmallImageSource` 属性,那么 `Label` 属性将不再是可选的,而是强制性的,并且 `RibbonButton` 或 `RibbonSplitButton` 将只显示标签。

RibbonButtonRibbonSplitButton 的默认尺寸适配策略,例如,对于一个具有 LargeImageSource 属性和 SmallImageSource 属性且为所有 RibbonButtonRibbonSplitButton 定义的四按钮 RibbonGroup,等同于

<RibbonGroup.GroupSizeDefinitions>
    <RibbonGroupSizeDefinition>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
    </RibbonGroupSizeDefinition>
    <RibbonGroupSizeDefinition>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
    </RibbonGroupSizeDefinition>
    <RibbonGroupSizeDefinition>
        <RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
        <RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
    </RibbonGroupSizeDefinition>
    <RibbonGroupSizeDefinition IsCollapsed="True"/>
</RibbonGroup.GroupSizeDefinitions>

XAML 语法的局限性

X11/Xrw 实现

  • 目前,`ListView.View` 的子节点只支持 `GridView` 节点。
  • 目前,`RibbonTab` 的子节点只支持 `RibbonGroup` 节点。
  • 目前,`RibbonSplitButton` 的子节点只支持 `RibbonMenuItem` 节点。
  • 目前,`RibbonGroup` 控件必须至少包含一个 `RibbonButton` 或 `RibbonSplitButton` 控件。不支持其他子节点。
  • 目前不支持 `RibbonGroup.GroupSizeDefinitions` 节点。

Microsoft® 原版实现

RelayCommand 的连接

要连接“RelayCommand”,必须满足以下几个要求。

  • 控件或其父控件之一必须设置 `DataContext` 属性,例如:`DataContext="{StaticResource MainViewModel}"`
  • 控件必须将 `Command` 属性绑定到作为数据上下文设置的类的属性,例如:`Command="{Binding RibbonTabHelp_LocalHelp_Command}"`
  • 作为数据上下文设置的类必须提供一个属性来访问“RelayCommand”,例如
        /// <summary>The relay command for Ribbon.TabHelp.GroupHelp.LocalHelp: Exit</summary>
        private RelayCommand _ribbonTabHelp_LocalHelp_Command = null;

        ...
        
        /// <summary>Provide the relay command for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
        public System.Windows.Input.ICommand RibbonTabHelp_LocalHelp_Command
        { get { return _ribbonTabHelp_LocalHelp_Command; } }
  • “RelayCommand”必须关联一个“Action”和一个“CanAction”回调,例如
        _ribbonTabHelp_LocalHelp_Command  = new RelayCommand(RibbonTabHelp_LocalHelp_Action,
                                                             RibbonTabHelp_LocalHelp_CanAction);
  • Action 和 CanAction 回调必须实现,例如
        /// <summary>The action for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
        /// <param name="o">A parameter to use for any purpose.</param>
        private void RibbonTabHelp_LocalHelp_Action(object o)
        {
            //System.Windows.Result result =
            MessageBox.Show("To do: Fill the '" + CLASS_NAME + ".RibbonTabHelp_LocalHelp_Action' " +
                            "stub with meaningful code!", "Unfinished",
                            MessageBoxButton.OKCancel, MessageBoxImage.Information);
        }

        /// <summary>The can-action property for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
        /// <param name="o">A parameter to use for any purpose.</param>
        /// <returns>True, if action can happen, or false otherwise.</returns>
        private bool RibbonTabHelp_LocalHelp_CanAction(object o)
        {
            return true;
        }

代码隐藏 (MainView.xaml.cs)

主视图对应的 C# 代码文件是 MainView.xaml.cs。它几乎是空的。

using System;

using X11;
using Xrw;
using XrwXAML;

namespace XamlRibbonApp
{
    /// <summary>The main window of the application. This class must be derived from XrwXAML.Window.
    /// It must be a partial class.
    /// The second part of the class will be autogenerated and named '*.generated.cs'.</summary>
    public partial class MainView : XrwXAML.RibbonWindow, IView
    {
        
        /// <summary>The default constructor.</summary>
        public MainView ()
            : base (-1, -1)
        {
            InitializeComponent ();
        }
        
        // =======================================================================
        // ATTENTION: On an accurate implemented MVVM pattern this class is empty!
        // =======================================================================

    }
}

主视图模型文件上下文

MainWindowViewModel.cs 文件包含所有连接到应用程序菜单项和 Ribbon 按钮的“RelayCommand”。它们中的大多数只是存根。只有两个被实现,以展示如何调用 OpenFileDialogSaveFileDialog

/// <summary>The action for Ribbon.ApplicationMenu.RibbonApplicationMenuItem.AppMenuItem1</summary>
/// <param name="o">A parameter to use for any purpose.</param>
private void RibbonAppMenu_FileOpen_Action(object o)
{
    // https://codeproject.org.cn/Articles/36745/Showing-Dialogs-When-Using-the-MVVM-Pattern
    OpenFileDialog ofd = new OpenFileDialog ();
    ofd.Filter = "comma separated values CSV|*.csv";
    ofd.FilterIndex = 0;
    ofd.InitialDirectory = (new System.IO.FileInfo (
                            System.Reflection.Assembly.GetExecutingAssembly().Location)).Directory.FullName;
    if (ofd.ShowDialog (Application.Current.MainWindow) == true)
    {
        int row = 1;
        string line;

        DocumentRows.Clear();
        CurrentFilePath = ofd.FileName;
        System.IO.StreamReader file = new System.IO.StreamReader(CurrentFilePath);
        while ((line = file.ReadLine()) != null)
        {
            DocumentRows.Add (new DocumentLine (string.Format("{0:0000}", row), line));
            row++;
        }

        file.Close();
    }
}

/// <summary>The action for Ribbon.ApplicationMenu.RibbonApplicationMenuItem.AppMenuItem3</summary>
/// <param name="o">A parameter to use for any purpose.</param>
private void RibbonAppMenu_FileSaveAs_Action(object o)
{
    // https://codeproject.org.cn/Articles/36745/Showing-Dialogs-When-Using-the-MVVM-Pattern
    SaveFileDialog sfd = new SaveFileDialog ();
    if (sfd.ShowDialog (Application.Current.MainWindow) == true)
    {
        CurrentFilePath = sfd.FileName;
    }
}

此外,`RibbonAppMenu_FileOpen_Action()` 回调通过 `DocumentRows.Clear()` 和 `DocumentRows.Add(...)` 更新 ViewModel 背后的 MVVM 模型。

`DocumentRows` 是 ViewModel 类的一个属性

/// <summary>Get the item collection, contained in the cocument.</summary>
public ObservableCollection<DocumentLine> DocumentRows
{
    get
    {
        if (_model != null)
            return _model.DocumentContent;
        else
        {
            SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
                              "DocumentRows.get (): Missing the model, connected to the view-model.");
            return new ObservableCollection<DocumentLine>();
        }
    }
    set
    {
        if (_model != null)
        {
            _model.DocumentContent = value;
            RaisePropertyChanged("DocumentRows");
        }
        else
            SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
                              "DocumentRows.set (): Missing the model, connected to the view-model.");
    }
}

将文档行实现为 `ObservableCollection<DocumentLine>` 提供了 `INotifyCollectionChanged` 和 `INotifyPropertyChanged` 的实现。而且 `DocumentRows` 属性不仅方便访问文档内容,还通过 XAML 连接到 `ListView` 的项源:`ItemsSource="{Binding DocumentRows}"`。

因此,文档内容的更改会自动更新视图。

`CurrentFilePath` 是 ViewModel 类的另一个属性

/// <summary>Get or set the current file path, stored inside the underlaying model.</summary>
public string CurrentFilePath
{
    get
    {
        if (_model != null)
            return _model.DocumentFilePath;
        else
        {
            SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
                              "CurrentFilePath.get (): Missing the model, connected to the view-model.");
            return "";
        }
    }
    set
    {
        if (_model != null)
        {
            _model.DocumentFilePath = value;
            RaisePropertyChanged("CurrentFilePath");
        }
        else
            SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
                              "CurrentFilePath.set (): Missing the model, connected to the view-model.");
    }
}

由于 MVVM ViewModel 实现了 `INotifyPropertyChanged` 并且 `CurrentFilePath` 属性调用了 `RaisePropertyChanged`,所以 `Label` “StateLine”会收到更改通知,因为它的 `Content` 属性通过 XAML 连接到 `CurrentFilePath`:`Content="{Binding CurrentFilePath}"`

有两篇 Code Project 文章,我推荐阅读它们,它们与干净的 MVVM 实现相关:

关注点

尽管我对 XAML 持怀疑态度(又是微软®的一项新技术,在文档-视图范式和模型-视图-控制器范式之后又是新的范式,所有控件都是新的),但我不得不承认

  • XAML 省去了大量的 GUI 编码!!!并且
  • Linux/Unix (X11) 和 Windows 的并行开发竟然如此简单!!!

因为为 X11 编写 XAML ribbon 应用程序已经成功,这不会是最后一篇关于 XAML 编程的文章。

自2014年11月23日起,文章为 X11 编写具有海量数据绑定和零代码的 XAML 应用程序延续了 XAML 主题。
自2015年2月2日起,文章为 X11 编写 XAML 计算器应用程序延续了 XAML 主题。

历史

这是2014年10月29日的第一个版本。
这是2014年11月23日的修订版。

© . All rights reserved.