views:

86

answers:

3

Hi everyone,

I have some XAML

<ItemsControl Name="mItemsControl">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBox Text="{Binding Mode=OneWay}" KeyUp="TextBox_KeyUp"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

that's bound to a simple ObservableCollection

private ObservableCollection<string> mCollection = new ObservableCollection<string>();

public MainWindow()
{
    InitializeComponent();

    this.mCollection.Add("Test1");
    this.mCollection.Add("Test2");
    this.mItemsControl.ItemsSource = this.mCollection;
}

Upon hitting the enter key in the last TextBox, I want another TextBox to appear. I have code that does it, but there's a gap:

private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key != Key.Enter)
    {
        return;
    }

    TextBox textbox = (TextBox)sender;

    if (IsTextBoxTheLastOneInTheTemplate(textbox))
    {
        this.mCollection.Add("A new textbox appears!");
    }
}

The function IsTextBoxTheLastOneInTheTemplate() is something that I need, but can't figure out how to write. How would I go about writing it?

I've considered using ItemsControl.ItemContainerGenerator, but can't put all the pieces together.

Thanks!

-Mike

A: 

I'm assuming this is a simplified version of what you're working on. A textbox with oneway binding to a string collection doesn't make sense to me.

The main problem in this case is using a simple string as the item source. I'm assuming we can't guarantee the strings will be unique so we can't draw any conclusions from textbox.Text. Also, since strings are immutable, we can't use the instance of the string to infer anything.

The first step in the solution is to create a class to hold the data that we can refer to. (This seems a little silly in this case as it just holds a string.)

    class MyData
    {
        public string Value { get; set; }
    }

Your second code block becomes:

    ObservableCollection<MyData> mCollection = new ObservableCollection<MyData>();

    public MainWindow()
    {
        InitializeComponent();

        this.mCollection.Add(new MyData { Value = "Test1" });
        this.mCollection.Add(new MyData { Value = "Test2" });
        this.mItemsControl.ItemsSource = this.mCollection;
    }

We'll use the Tag property of the textbox to store a reference to our binding source. We'll use this to get around the uniqueness issues. The XAML becomes:

    <ItemsControl Name="mItemsControl">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBox Text="{Binding Value}" Tag="{Binding}" KeyUp="TextBox_KeyUp"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

Lastly, the handler becomes:

    private void TextBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key != Key.Enter)
        {
            return;
        }

        TextBox textbox = (TextBox)sender;

        if (mItemsControl.Items.IndexOf(textbox.Tag) == mItemsControl.Items.Count - 1)
        {
            this.mCollection.Add(new MyData() { Value = "A new textbox appears!" });
        }
    }
Scott J
A: 

I was able to get a decent solution by referring to http://drwpf.com/blog/2008/07/20/itemscontrol-g-is-for-generator/. Not super-elegant, but it worked for me.

    private void TextBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key != Key.Enter)
        {
            return;
        }

        TextBox textbox = (TextBox)sender;

        var lastContainer = this.mItemsControl.ItemContainerGenerator.ContainerFromIndex(this.mItemsControl.Items.Count - 1);

        var visualContainer = (Visual)lastContainer;

        var containedTextbox = (TextBox)GetDescendantByType(visualContainer, typeof(TextBox));

        var isSame = textbox == containedTextbox;

        if (isSame)
        {
             this.mCollection.Add("A new textbox appears!");
        }
    }


    public static Visual GetDescendantByType(Visual element, Type type)
    {
        if (element.GetType() == type) return element;

        Visual foundElement = null;

        if (element is FrameworkElement)
            (element as FrameworkElement).ApplyTemplate();

        for (int i = 0;
            i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            Visual visual = VisualTreeHelper.GetChild(element, i) as Visual;
            foundElement = GetDescendantByType(visual, type);
            if (foundElement != null)
                break;
        }

        return foundElement;
    }
Mike
A: 

It seems to me that this is behavior best defined in the view model:

public class ItemCollection : ObservableCollection<Item>
{
    public ItemCollection()
    {
        // this guarantees that any instance created always has at least one
        // item in it - you don't need this if you're creating instances in
        // code, but if you just create them in XAML you do.
        Item item = new Item(this);
        Add(item);
    }
}

public class Item
{
    internal Item(ItemCollection owner)
    {
        Owner = owner;
    }

    public bool IsLast
    {
        get
        {
            return Owner.LastOrDefault() == this;
        }
    }

    private ItemCollection Owner { get; set; }

    private string _Value;

    // here's the actual behavior:  if the last item in the collection is
    // given a non-empty Value, a new item gets added after it.
    public string Value
    {
        get { return _Value; }
        set
        {
            _Value = value;
            if (IsLast && !String.IsNullOrEmpty(_Value))
            {
                Owner.Add(new Item(Owner));
            }
        }
    }
}

From here, it's a simple matter of making the TextBox update its source when the user presses ENTER:

<DataTemplate DataType="{x:Type local:Item}">
    <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
             KeyUp="TextBox_KeyUp"/>
</DataTemplate>

With the KeyUp event handler:

private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key != Key.Enter)
    {
        return;
    }

    TextBox t = (TextBox)sender;
    BindingExpression be = t.GetBindingExpression(TextBox.TextProperty);
    be.UpdateSource();
}
Robert Rossney