【问题标题】:How to Create a Generic ListBox Dialog Control如何创建通用列表框对话框控件
【发布时间】:2014-09-30 19:41:12
【问题描述】:

Skip to answer to see how to implement the ListDialogBox

我有一个可重复使用的对话框/窗口,提示用户从列表框中选择一个项目,点击“确定”以确认选择。

效果很好;但是,列表框不知道它正在使用什么类型的数据。因此,列表绑定到一个ObservableCollection<Object>,可以由对话框的调用者设置。

此外,列表框有一个自定义项目模板,允许用户从列表中删除项目。

这是我正在描述的对话框:

理想情况下,我想将DisplayMemberPath 用于列表框,但我不被允许,因为我正在创建自己的项目模板。这是一个问题,因为调用者应该能够指定他/她想要绑定到我设置的自定义项模板的属性。

由于这种方法行不通,我的第一个问题是:

1。我可以在运行时指定数据绑定值的路径吗?

在 XAML 中,我希望看到这样的内容,但这是错误的:

<ListBox.ItemTemplate>
    <Label Content="{Binding Path={Binding CustomPath}}"/>
    <Button Width="20" Height="20" FontWeight="Bold" Content="×"/>
</ListBox.ItemTemplate>

(为简洁起见省略了一些属性)

假设第一个问题解决了,我还有另一个问题。列表框正在使用非泛型类型Object,它没有调用者想要绑定的属性。列表框无法将对象转换为自定义类型并访问所需的属性。这引出了我的第二个问题。

2。如何指示 ListBox 能够使用未知数据类型,但能够选择数据绑定值的路径?

也许这应该留给关于 SO 的另一个问题,但是能够指定绑定是使用 ToString() 还是属性会很好。


我能想到的唯一解决方案是创建一个接口,该接口具有调用者必须使用的属性(名为DisplayText)。然后该列表将绑定到 ObservableCollection&lt;CustomInterface&gt; 的一个实例。

但是,不希望将已经存在的数据类型包装到此接口中,这样才能正常工作。有没有更好的方法来做到这一点?


编辑:实施者如何使用 ListDialogBox

以下是我希望调用者能够设置对话框的方式(或类似的简单方式):

public CustomItem PromptForSelection()
{
    ListDialogBox dialog = new ListDialogBox();
    dialog.Items = GetObservableCollection();
    dialog.ListDisplayMemberPath = "DisplayName";
    dialog.ShowDialog();
    if(!dialog.IsCancelled)
    {
        return (CustomItem) dialog.SelectedItem;
    }
}

public ObservableCollection<Object> GetObservableCollection()
{
    ObservableCollection<Object> coll = new ObservableCollection<Object>();

    CustomItem item = new CustomItem(); 
    item.DisplayName = "Item1";
    CustomItem item2 = new CustomerItem();
    item2.DisplayName = "Item2";
    //...

    coll.Add(item);
    coll.Add(item2);
    //...

    return coll;
}

代码将不起作用,因为如果 ObservableCollection&lt;Object&gt; 用于 ListDialogBox,则 DisplayName 属性没有意义。 这是因为Object 没有定义该属性。

ListDialogBox 类中,我想将项目模板的标签绑定到DisplayName 属性,因为这是提供的ListDisplayMemberPath 值。

我该如何克服这个问题?

【问题讨论】:

  • 也许我没有正确理解您的问题,但 string 是标签可以绑定的唯一类型吗?如果是这样,为什么不直接创建一个包含字符串属性的类以供 ListBox 绑定? However, it isn't desired to wrap already existing data types into this interface just so this works 为什么不呢?接口是两个或多个实体之间的契约。除非有更多内容,否则我看不出有什么问题。
  • ListBox 应该能够绑定到任何数据类型;但是它将始终呈现为字符串。我希望能够在运行时指定绑定的路径,因为可能存在多个使用不同数据类型的对话框。例如,一个用于选择FileInfo 对象的对话框,另一个用于选择Person 对象的对话框。到目前为止,我正在使用一个界面,因为它似乎是做到这一点的唯一方法。不幸的是,对于实现者来说,这似乎需要做很多额外的工作,因为需要一个包装类来绑定到已经存在的类属性。

标签: c# wpf generics wpf-controls


【解决方案1】:

此答案旨在解决原始问题中的问题,并为未来的读者提供如何实施ListDialogBox 的示例。

原始问题中的问题涉及能够指定如何在ListBox 中显示信息。由于ListBox 直到运行时才知道它正在显示什么类型的数据,因此没有一种直接的方法来指定指向所显示的所需属性的“路径”。

这个问题最简单的解决方案是创建一个ListDialogBox 独占使用的接口,然后调用者只需要创建该接口的实例来自定义信息的显示方式。

此解决方案的唯一缺点是调用者需要满足他/她的数据以符合ListDialogBox;但是,这很容易实现。


如何创建和实现ListDialogBox

ListDialogBox 的目标是类似于 OpenFileDialogSaveFileDialog,因为您初始化对话框,提示输入结果,然后处理结果。

首先,我将展示和解释ListDialogBox 的代码(XAML 和代码隐藏)。
下面的 XAML 已经过修剪,只显示对话框的结构和必要的属性。

<Window
    //You must specify the namespace that contains the the converters used by
    //this dialog
    xmlns:local="clr-namespace:<your-namespace>"
    //[Optional]: Specify a handler so that the ESC key closes the dialog.
    KeyDown="Window_KeyDown">
<Window.Resources>
    //These converters are used to control the dialog box.
    <BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
    <local:NullToBooleanConverter x:Key="NullToBool"/>
</Window.Resources>
<Grid>
     //This displays a custom prompt which can be set by the caller.
    <TextBlock Text="{Binding Prompt}" TextWrapping="Wrap" />

    //The selection button is only enabled if a selection is made (non-null)
    <Button IsEnabled="{Binding Path=SelectedItem, 
                                ElementName=LstItems,
                                Converter={StaticResource NullToBool}}" 
        //Display a custom message for the select button.
        Content="{Binding SelectText}" 
        //Specify a handler to close the dialog when a selection is confirmed.
        Click="BtnSelect_Click" Name="BtnSelect" />

    //The cancel button specifies a handler to close the dialog.
    <Button Content=" Cancel" Name="BtnCancel" Click="BtnCancel_Click" />

    //This list box displays the items by using the 'INamedItem' interface
    <ListBox ItemsSource="{Binding Items}" Name="LstItems"        
             ScrollViewer.HorizontalScrollBarVisibility="Disabled">
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="HorizontalContentAlignment"  Value="Stretch"/>
            </Style>
        </ListBox.ItemContainerStyle>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <DockPanel>

            <Button DockPanel.Dock="Right" 

            //The delete button is only available when the 'CanRemoveItems'
            //property  is true.  See usage for more details.
            Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, 
                                    Path=CanRemoveItems, 
                                    Converter={StaticResource BoolToVisibility}}" 
            //Visual properties for correctly displaying the red 'x'.
            //The 'x' is actually the multiplication symbol: '×'
            FontFamily="Elephant" Foreground="Red" FontWeight="Bold" FontStyle="Normal" 
            FontSize="18" Padding="0,-3,0,0" Content="×" 
            //[Optional]: Align button on the right end.
            HorizontalAlignment="Right" 
            //Specify handler that removes the item from the list (internally)
            Click="BtnRemove_Click" />

            //The DockPanel's last child fills the remainder of the template
            //with the one and only property from the INamedItem interface.
            <Label Content="{Binding DisplayName}"                          
                //[Optional]: This handler allows double-clicks to confirm selection.
                MouseDoubleClick="LstItem_MouseDoubleClick"/>

                </DockPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

NullToBooleanConverter 与 SO 上的this answer 基本相同。它用于根据ListBox.SelectedItem 是否为null 来启用/禁用确认选择按钮。此转换器的不同之处在于,当转换后的值为NOT null时,它返回true

ListDialogBox代码隐藏:

该类定义了调用者可以修改以自定义方式的所有属性 显示的ListDialogBox 及其功能。

public partial class ListDialogBox : Window, INotifyPropertyChanged
{   
    /* The DataContext of the ListDialogBox is itself.  It implements
     * INotifyPropertyChanged so that the dialog box bindings are updated when
     * the caller modifies the functionality.
     */
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

    /* Optionally, the ListDialogBox provides a callback mechanism that allows
     * the caller to cancel the removal of any of the items.
     * See usage for more details.
     */
    public event RemoveItemEventHandler RemoveItem;
    protected void RaiseRemoveItem(RemoveItemEventArgs args)
    {
        if (RemoveItem != null)
        {
            RemoveItem(this, args);
        }
    }

    //Local copies of all the properties. (with default values)
    private string prompt = "Select an item from the list.";
    private string selectText = "Select";
    private bool canRemoveItems = false;
    private ObservableCollection<INamedItem> items;
    private INamedItem selectedItem = null;

    public ListDialogBox()
    {
        InitializeComponent();
        DataContext = this;  //The DataContext is itself.
    }

    /* Handles when an item is double-clicked.
     * The ListDialogBox.SelectedItem property is set and the dialog is closed.
     */
    private void LstItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        SelectedItem = ((FrameworkElement)sender).DataContext as INamedItem;
        Close();
    }

    /* Handles when the confirm selection button is pressed.
     * The ListDialogBox.SelectedItem property is set and the dialog is closed.
     */        
    private void BtnSelect_Click(object sender, RoutedEventArgs e)
    {
        SelectedItem = LstItems.SelectedItem as INamedItem;
        Close();
    }

    /* Handles when the cancel button is pressed.
     * The lsitDialogBox.SelectedItem remains null, and the dialog is closed.
     */
    private void BtnCancel_Click(object sender, RoutedEventArgs e)
    {
        Close();
    }

    /* Handles when any key is pressed.  Here we determine when the user presses
     * the ESC key.  If that happens, the result is the same as cancelling.
     */
    private void Window_KeyDown(object sender, KeyEventArgs e)
    {   //If the user presses escape, close this window.
        if (e.Key == Key.Escape)
        {
            Close();
        }
    }

    /* Handles when the 'x' button is pressed on any of the items.
     * The item in question is found and the RemoveItem event subscribers are notified.
     * If the subscribers do not cancel the event, then the item is removed.
     */
    private void BtnRemove_Click(object sender, RoutedEventArgs e)
    {   //Obtain the item that corresponds to the remove button that was clicked.
        INamedItem removeItem = ((FrameworkElement)sender).DataContext as INamedItem;

        RemoveItemEventArgs args = new RemoveItemEventArgs(removeItem);
        RaiseRemoveItem(args);

        if (!args.Cancel)
        {   //If not cancelled, then remove the item.
            items.Remove(removeItem);
        }
    }

    //Below are the customizable properties.

    /* This property specifies the prompt that displays at the top of the dialog. */
    public string Prompt
    {
        get { return prompt; }
        set
        {
            if (prompt != value)
            {
                prompt = value;
                RaisePropertyChanged("Prompt");
            }
        }
    }

    /* This property specifies the text on the confirm selection button. */
    public string SelectText
    {
        get { return selectText; }
        set
        {
            if (selectText != value)
            {
                selectText = value;
                RaisePropertyChanged("SelectText");
            }
        }
    }

    /* This property controls whether or not items can be removed.
     * If set to true, the the 'x' button appears on the ItemTemplate.
     */
    public bool CanRemoveItems
    {
        get { return canRemoveItems; }
        set
        {
            if (canRemoveItems != value)
            {
                canRemoveItems = value;
                RaisePropertyChanged("CanRemoveItems");
            }
        }
    }

    /* This property specifies the collection of items that the user can select from.
     * Note that this uses the INamedItem interface.  The caller must comply with that
     * interface in order to use the ListDialogBox.
     */
    public ObservableCollection<INamedItem> Items
    {
        get { return items; }
        set
        {
            items = value;
            RaisePropertyChanged("Items");
        }
    }

    //Below are the read only properties that the caller uses after
    //prompting for a selection.

    /* This property contains either the selected INamedItem, or null if
     * no selection is made.
     */
    public INamedItem SelectedItem
    {
        get { return selectedItem; }
        private set
        {
            selectedItem = value;
        }
    }

    /* This property indicates if a selection was made.
     * The caller should check this property before trying to use the selected item.
     */
    public bool IsCancelled
    {   //A simple null-check is performed (the caller can do this too).
        get { return (SelectedItem == null); }
    }
}

//This delegate defines the callback signature for the RemoveItem event.
public delegate void RemoveItemEventHandler(object sender, RemoveItemEventArgs e);

/* This class defines the event arguments for the RemoveItem event.
 * It provides access to the item being removed and allows the event to be cancelled.
 */  
public class RemoveItemEventArgs
{
    public RemoveItemEventArgs(INamedItem item)
    {
        RemoveItem = item;
    }

    public INamedItem RemoveItem { get; private set; }
    public bool Cancel { get; set; }
}

INamedItem接口:

现在ListDialogBox 已经出现了,我们需要看看调用者如何使用它。如前所述,最简单的方法是创建一个界面。

INamedItem 接口仅提供一个属性(称为DisplayName),ListDialogBox 需要这些属性的列表才能显示信息。 ListDialogBox 依赖于调用者为这个属性设置一个有意义的值。

界面非常简单:

public interface INamedItem
{
    string DisplayName { get; set; }
}

用法:

此时,与ListDialogBox 的功能相关的所有类都已涵盖,现在是时候在程序中查看和实现它了。

为此,我们需要实例化ListDialogBox,然后设置自定义任何所需的属性。

ListDialogBox dialog = new ListDialogBox();
dialog.Prompt = "Select a pizza topping to add from the list below:";
dialog.SelectText = "Choose Topping";
dialog.CanRemoveItems = true; //Setting to false will hide the 'x' buttons.

ListDialogBox 需要 ObservableCollection&lt;INamedItem&gt;,因此我们必须先生成它才能继续。为此,我们为要使用的数据类型创建一个“包装类”。在此示例中,我将创建一个实现 INamedItemStringItem 类并将 DisplayName 设置为任意字符串。见下文:

public class StringItem : INamedItem
{    //Local copy of the string.
    private string displayName;

    //Creates a new StringItem with the value provided.
    public StringItem(string displayName)
    {   //Sets the display name to the passed-in string.
        this.displayName = displayName;
    }

    public string DisplayName
    {   //Implement the property.  The implementer doesn't need
        //to provide an implementation for setting the property.
        get { return displayName; }
        set { }
    }
}

然后使用StringItem 创建ObservableCollection&lt;INamedItem&gt;

ObservableCollection<INamedItem> toppings = new ObservableCollection<INamedItem>();
toppings.Add(new StringItem("Pepperoni"));
toppings.Add(new StringItem("Ham"));
toppings.Add(new StringItem("Sausage"));
toppings.Add(new StringItem("Chicken"));
toppings.Add(new StringItem("Mushroom"));
toppings.Add(new StringItem("Onions"));
toppings.Add(new StringItem("Olives"));
toppings.Add(new StringItem("Bell Pepper"));
toppings.Add(new StringItem("Pineapple"));

//Now we can set the list property:
dialog.Items = toppings;

至此,基本实现已经设置完毕。我们只需要调用dialog.ShowDialog(),并处理结果。但是,由于该示例允许用户从列表中删除项目,我们可能需要提示确认。为此,我们需要订阅RemoveItem 事件。

RemoveItemEventHandler myHandler = (object s, RemoveItemEventArgs args) =>
{
    StringItem item = args.RemoveItem as StringItem;
    MessageBoxResult result = MessageBox.Show("Are you sure that you would like" + 
        " to permanently remove \"" + item.DisplayName + "\" from the list?",
        "Remove Topping?", 
        MessageBoxButton.YesNo, MessageBoxImage.Question);

    if (result == MessageBoxResult.No)
    {    //The user cancelled the deletion, so cancel the event as well.
        args.Cancel = true;
    }
};

//Subscribe to RemoveItem event.
dialog.RemoveItem += myHandler;

最后,我们可以显示ListDialogBox 并处理结果。我们还必须记得取消订阅RemoveItem 事件:

dialog.ShowDialog();
dialog.RemoveItem -= myHandler;

//Process the result now.
if (!dialog.IsCancelled)
{
    StringItem item = dialog.SelectedItem as StringItem;
    MessageBox.Show("You added the topping \"" + item.DisplayName +
        "\" to your pizza!");
}

剩下的就是将此代码放入您的应用程序并自行运行。 上面的示例创建了以下ListDialogBox

此外,当点击意大利辣香肠上的“x”时,会显示提示:

【讨论】:

    【解决方案2】:

    我可以在运行时指定数据绑定值的路径吗?在 XAML 中,我希望看到这样的内容,但这是错误的:

    <ListBox.ItemTemplate>
         <Label Content="{Binding Path={Binding CustomerPath}}"/>
         <Button Width="20" Height="20" FontWeight="Bold" Content="×"/>
     </ListBox.ItemTemplate>
    

    绑定已经错了。如果您的ListBox's ItemsSourceCustomers 并且您想绑定到它的内容,那么只需使用{Binding Path=CustomerPath}。您的问题的答案是肯定的,您可以在运行时指定数据绑定值的路径。您必须为ListBox 加载每个Template,然后在运行时设置bindings

    伪代码:

    1. 加载项目模板
    2. 获取标签控件
    3. 将绑定设置为调用者设置属性以将其绑定到的内容

    如何指示 ListBox 能够使用未知数据类型,但能够选择数据绑定值的路径?

    Binding 将在绑定到的class 上调用ToString()。所以如果你的绑定是正确的并且假设你有Customer作为对象

    <Label Content="{Binding .}"/>
    

    如果您没有覆盖 Customer 对象的 ToString(),它将显示默认的 ToString(),但如果您这样做,那么它将是 Label 中显示的那个。这是一个丑陋的解决方案,只是用它代替ListBox 给出的DisplayMemberPath

    理想情况下,我会在运行时设置绑定而不是创建接口,这不仅是因为您所说的,而且将用于此控件的后续对象必须实现该接口。

    【讨论】:

    • 糟糕。我很抱歉,但我的帖子应该说CustomPath(现已修复),因此暗示路径直到运行时才知道。尽管实现者可以使用Customer。能否详细说明加载项目模板和设置绑定的过程?我想这是在 C# 端完成的,也许是在初始化对话框时。
    • 我需要更多关于你希望它如何实现的细节。 caller 是要启动对话框并传递绑定它的属性的人吗?
    • 正确。调用者负责显示对话框、设置对象列表以及要绑定的属性。
    • 你能给我一个示例,你如何传递要绑定的属性
    • 我已经编辑了帖子。我希望这能解决问题。
    猜你喜欢
    • 2015-02-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-19
    相关资源
    最近更新 更多