演练:实现就地编辑

更新:2007 年 11 月

本演练演示如何为 Windows Presentation Foundation (WPF) 自定义控件实现就地编辑。可以在 Visual Studio Windows Presentation Foundation (WPF) 设计器中使用此设计时功能为自定义按钮控件设置 Content 属性值。在本演练中,此控件是一个简单的按钮,装饰器是一个用来更改该按钮的内容的文本框。

在本演练中,您将执行下列任务:

  • 创建一个 WPF 自定义控件库项目。

  • 为设计时元数据创建一个单独的程序集。

  • 为就地编辑实现装饰提供程序。

  • 在设计时测试控件。

完成本演练后,您将知道如何为自定义控件创建装饰器提供程序。

说明:

显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您的当前设置或版本。若要更改设置,请在“工具”菜单上选择“导入和导出设置”。有关更多信息,请参见 Visual Studio 设置

先决条件

您需要以下组件来完成本演练:

  • Visual Studio 2008。

创建自定义控件

第一步是为自定义控件创建项目。该控件是一个带有少量设计时代码的简单按钮,该按钮使用 GetIsInDesignMode 方法来实现设计时行为。

创建自定义控件

  1. 使用 Visual C# 创建一个名为 CustomControlLibrary 的新 WPF 自定义控件库项目。

    CustomControl1 的代码在“代码编辑器”中打开。

  2. 在“解决方案资源管理器”中,将代码文件的名称改为 DemoControl.cs。如果出现询问是否要对此项目中的所有引用执行重命名操作的消息框,请单击“是”。

  3. 在“解决方案资源管理器”中展开 Themes 文件夹。

  4. 双击 Generic.xaml。

    Generic.xaml 在 WPF 设计器中打开。

  5. 在 XAML 视图中,将出现的所有“CustomControl1”都替换为“DemoControl”。

  6. 在代码编辑器中打开 DemoControl.cs。

  7. 用下面的代码替换自动生成的代码。DemoControl 自定义控件继承自 Button

    using System;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace CustomControlLibrary
    {
        public class DemoControl : Button
        {   
        }
    }
    
  8. 将项目的输出路径设置为“bin\”。

  9. 生成解决方案。

创建设计时元数据程序集

设计时代码在特定元数据程序集中部署。有关更多信息,请参见如何:使用元数据存储区。本演练中的自定义装饰器仅受 Visual Studio 支持,而且部署到名为 CustomControlLibrary.VisualStudio.Design 的程序集内。

创建设计时元数据程序集

  1. 使用 Visual C# 为解决方案添加一个名为 CustomControlLibrary.VisualStudio.Design 的新类库项目。

  2. 将项目的输出路径设置为“..\CustomControlLibrary\bin\”。这样可使控件的程序集与元数据程序集位于同一文件夹中,从而可为设计器启用元数据发现。

  3. 添加对下列 WPF 程序集的引用。

    • PresentationCore

    • PresentationFramework

    • WindowsBase

  4. 添加对下列 WPF 设计器程序集的引用。

    • Microsoft.Windows.Design

    • Microsoft.Windows.Design.Extensibility

    • Microsoft.Windows.Design.Interaction

  5. 添加对 CustomControlLibrary 项目的引用。

  6. 在“解决方案资源管理器”中,将 Class1 代码文件的名称改为 Metadata.cs。

  7. 用下面的代码替换自动生成的代码。此代码将创建一个将自定义设计时实现附加到 DemoControl 类的 AttributeTable

    using System;
    using Microsoft.Windows.Design.Features;
    using Microsoft.Windows.Design.Metadata;
    
    namespace CustomControlLibrary.VisualStudio.Design
    {
        // Container for any general design-time metadata to initialize.
        // Designers look for a type in the design-time assembly that 
        // implements IRegisterMetadata. If found, designers instantiate 
        // this class and call its Register() method automatically.
        internal class Metadata : IRegisterMetadata
        {
            // Called by the designer to register any design-time metadata.
            public void Register()
            {
                AttributeTableBuilder builder = new AttributeTableBuilder();
    
                // Add the adorner provider to the design-time metadata.
                builder.AddCustomAttributes(
                    typeof(DemoControl),
                    new FeatureAttribute(typeof(InplaceButtonAdorners)));
    
                MetadataStore.AddAttributeTable(builder.CreateTable());
    
            }
        }
    }
    
  8. 保存解决方案。

实现装饰器提供程序

装饰器提供程序是在名为 InplaceButtonAdorners 的类型中实现的。此装饰器提供程序使用户可以在设计时设置控件的 Content 属性。

实现装饰器提供程序

  1. 向 CustomControlLibrary.VisualStudio.Design 项目中添加一个名为 InplaceButtonAdorners 的新类。

  2. 在 InplaceButtonAdorners 的代码编辑器中,用下面的代码替换自动生成的代码。此代码实现的 PrimarySelectionAdornerProvider 提供一个基于 TextBox 控件的装饰器。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Shapes;
    using Microsoft.Windows.Design.Interaction;
    using System.Windows.Data;
    using System.Windows.Input;
    using System.ComponentModel;
    using Microsoft.Windows.Design.Model;
    
    namespace CustomControlLibrary.VisualStudio.Design
    {
        // The InplaceButtonAdorners class provides two adorners:  
        // an activate glyph that, when clicked, activates in-place 
        // editing, and an in-place edit control, which is a text box.
        internal class InplaceButtonAdorners : PrimarySelectionAdornerProvider
        {
            private Rectangle activateGlyph;
            private TextBox editGlyph;
            private AdornerPanel adornersPanel;
    
            public InplaceButtonAdorners()
            {
                adornersPanel = new AdornerPanel();
                adornersPanel.IsContentFocusable = true;
                adornersPanel.Children.Add(ActivateGlyph);
    
                Adorners.Add(adornersPanel);
            }
    
            private UIElement ActivateGlyph
            {
                get
                {
                    if (activateGlyph == null)
                    {
                        // The following code specifies the shape of the activate 
                        // glyph. This can also be implemented by using a XAML template.
                        Rectangle glyph = new Rectangle();
                        glyph.Fill = AdornerColors.HandleFillBrush;
                        glyph.Stroke = AdornerColors.HandleBorderBrush;
                        glyph.RadiusX = glyph.RadiusY = 2;
                        glyph.Width = 10;
                        glyph.Height = 5;
                        glyph.Cursor = Cursors.Hand;
    
                        ToolTipService.SetToolTip(
                            glyph, 
                            "Click to edit the text of the button.  " + 
                            "Enter to commit, ESC to cancel.");
    
                        // Position the glyph to the upper left of the DemoControl, 
                        // and slightly inside.
                        AdornerPlacementCollection placement = new AdornerPlacementCollection();
                        placement.PositionRelativeToContentHeight(0, 10);
                        placement.PositionRelativeToContentWidth(0, 5);
                        placement.SizeRelativeToAdornerDesiredHeight(1, 0);
                        placement.SizeRelativeToAdornerDesiredWidth(1, 0);
    
                        AdornerPanel.SetPlacements(glyph, placement);
    
                        // Add interaction to the glyph.  A click starts in-place editing.
                        ToolCommand command = new ToolCommand("ActivateEdit");
                        Task task = new Task();
                        task.InputBindings.Add(new InputBinding(command, new ToolGesture(ToolAction.Click)));
                        task.ToolCommandBindings.Add(new ToolCommandBinding(command, OnActivateEdit));
                        AdornerProperties.SetTask(glyph, task);
                        activateGlyph = glyph;
                    }
    
                    return activateGlyph;
                }
            }
            // When in-place editing is activated, a text box is placed 
            // over the control and focus is set to its input task. 
            // Its task commits itself when the user presses enter or clicks 
            // outside the control.
            private void OnActivateEdit(object sender, ExecutedToolEventArgs args)
            {
                adornersPanel.Children.Remove(ActivateGlyph);
                adornersPanel.Children.Add(EditGlyph);
    
                // Once added, the databindings activate. 
                // All the text can now be selected.
                EditGlyph.SelectAll();
                EditGlyph.Focus();
    
                GestureData data = GestureData.FromEventArgs(args);
                Task task = AdornerProperties.GetTask(EditGlyph);
                task.Description = "Edit text";
                task.BeginFocus(data);
            }
    
            // The EditGlyph utility property creates a TextBox to use as 
            // the in-place editing control. This property centers the TextBox
            // inside the target control and sets up data bindings between 
            // the TextBox and the target control.
            private TextBox EditGlyph
            {
                get
                {
                    if (editGlyph == null)
                    {
                        TextBox glyph = new TextBox();
                        glyph.BorderThickness = new Thickness(0);
                        glyph.Margin = new Thickness(4);
    
                        AdornerPlacementCollection placement = new AdornerPlacementCollection();
                        placement.PositionRelativeToContentWidth(0, 0);
                        placement.PositionRelativeToContentHeight(0, 0);
                        placement.SizeRelativeToContentHeight(1, 0);
                        placement.SizeRelativeToContentWidth(1, 0);
    
                        AdornerPanel.SetPlacements(glyph, placement);
    
                        // Data bind the glyph's vertical and horizontal alignment
                        // to the target control's alignment properties.
                        Binding binding = new Binding();
                        binding.Source = glyph;
                        binding.Path = new PropertyPath(
                            "(0).(1)", 
                            AdornerProperties.ActualViewProperty, 
                            Button.HorizontalContentAlignmentProperty);
                        glyph.SetBinding(TextBox.HorizontalContentAlignmentProperty, binding);
    
                        binding = new Binding();
                        binding.Source = glyph;
                        binding.Path = new PropertyPath(
                            "(0).(1)", 
                            AdornerProperties.ActualViewProperty, 
                            Button.VerticalContentAlignmentProperty);
                        glyph.SetBinding(TextBox.VerticalContentAlignmentProperty, binding);
    
                        // Make the glyph's background match the control's background. 
                        binding = new Binding();
                        binding.Source = glyph;
                        binding.Path = new PropertyPath(
                            "(0).(1)", 
                            AdornerProperties.ActualViewProperty, 
                            Button.BackgroundProperty);
                        glyph.SetBinding(TextBox.BackgroundProperty, binding);
    
                        // Two-way data bind the text box's text property to content.
                        binding = new Binding();
                        binding.Source = glyph;
                        binding.Path = new PropertyPath("(0).(1)[Content].(2)",
                          AdornerProperties.ActualModelProperty,
                          TypeDescriptor.GetProperties(
                              typeof(ModelItem))["Properties"],
                              TypeDescriptor.GetProperties(
                                  typeof(ModelProperty))["ComputedValue"]);
                        binding.Mode = BindingMode.TwoWay;
                        binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
                        binding.Converter = new ContentConverter();
                        glyph.SetBinding(TextBox.TextProperty, binding);
    
                        // Create a task that describes the UI interaction.
                        ToolCommand commitCommand = new ToolCommand("Commit Edit");
                        Task task = new Task();
                        task.InputBindings.Add(
                            new InputBinding(
                                commitCommand, 
                                new KeyGesture(Key.Enter)));
    
                        task.ToolCommandBindings.Add(
                            new ToolCommandBinding(commitCommand, delegate
                        {
                            task.Complete();
                        }));
    
                        task.FocusDeactivated += delegate
                        {
                            adornersPanel.Children.Remove(EditGlyph);
                            adornersPanel.Children.Add(ActivateGlyph);
                        };
    
                        AdornerProperties.SetTask(glyph, task);
    
                        editGlyph = glyph;
                    }
    
                    return editGlyph;
                }
            }
    
            // The ContentConverter class ensures that only strings
            // are assigned to the Text property of EditGlyph.
            private class ContentConverter : IValueConverter
            {
                public object Convert(
                    object value, 
                    Type targetType, 
                    object parameter, 
                    System.Globalization.CultureInfo culture)
                {
                    if (value is string)
                    {
                        return value;
                    }
    
                    return string.Empty;
                }
    
                public object ConvertBack(
                    object value, 
                    Type targetType, 
                    object parameter, 
                    System.Globalization.CultureInfo culture)
                {
                    return value;
                }
            }
        }
    }
    
  3. 生成解决方案。

测试设计时实现

可以像使用任何其他 WPF 控件一样使用 DemoControl 类。WPF 设计器处理所有设计时对象的创建。

测试设计时实现

  1. 使用 Visual C# 向解决方案中添加一个名为 DemoApplication 的新 WPF 应用程序项目。

    Window1.xaml 在 WPF 设计器中打开。

  2. 添加对 CustomControlLibrary 项目的引用。

  3. 在 XAML 视图中,用以下 XAML 替换自动生成的 XAML。此 XAML 将添加对 CustomControlLibrary 命名空间的引用并添加 DemoControl 自定义控件。如果该控件未出现,可能需要单击设计器顶部的信息栏,以重新加载视图。

    <Window x:Class="DemoApplication.Window1"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:ccl="clr-namespace:CustomControlLibrary;assembly=CustomControlLibrary"
        Title="Window1" Height="300" Width="300">
        <Grid>
            <ccl:DemoControl></ccl:DemoControl>
        </Grid>
    </Window>
    
  4. 重新生成解决方案。

  5. 在“设计”视图中,单击 DemoControl 控件将其选中。

    在 DemoControl 控件的左上角出现一个小的 Rectangle 标志符号。

  6. 单击 Rectangle 标志符号激活就地编辑。

    随即出现一个文本框,其中显示了 DemoControl 的 Content。由于内容当前为空,因此只能看到按钮中心的光标。

  7. 键入文本内容的新值,然后按 Enter 键。

    在 XAML 视图中,Content 属性将设置为在“设计”视图中键入的文本值。

  8. 将 DemoApplication 项目设置为启动项目,并运行解决方案。

    在运行时,该按钮具有您用装饰器设置的文本值。

后续步骤

您可以向自定义控件中添加更多设计时功能。

请参见

其他资源

创建自定义编辑器

WPF 设计器扩展性