tags:

views:

2169

answers:

4

I have a listbox with a bunch of contols in each list item.

<ListBox x:Name="projectList" IsSynchronizedWithCurrentItem="True">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Name}" />
                <ListBox x:Name="taskList" ItemsSource="{Binding Tasks}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Text="{Binding Name}" />
                            </StackPanel>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
                <TextBox x:Name="textBoxTask" />
                <Button
                    x:Name="ButtonAddNewTask"
                    Content="Test"
                    CommandParameter="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext}"
                    Click="ButtonAddNewTask_Click"
                    />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

When I click on the button in the listbox i want to add a new item to the listbox within the listbox. I've come this far. So my question is how do I get hold of the textbox and how do I update the listbox?

Here is my click event

private void ButtonAddNewTask_Click(object sender, RoutedEventArgs e)
{
    Button button = (Button)sender;
    Project proj = button.DataContext as Project;
    if(proj.Tasks == null)
        proj.Tasks = new List<Task>();

    proj.Tasks.Add(new Task("Added Task"));
}

Thanx

+2  A: 

The easiest solution would likely be to have one object represent each item in the outer ListBox. It would then have properties that would represent each control in the item - the text in the TextBox, and the items in the ListBox (a list of Tasks, I think, based on your Click handler).

In your Click handler, you can get the Button's DataContext (which should be an item in the collection of the outer list), and add a new Task to that object's list of tasks. Since the inner ListBox is bound to that list, it should be updated with the new item (assuming that it sends events when items are added, such as with ObservableCollection).

Update: Based on your comments, the following should work.

Your Project class should have two properties:

class Project
{
    public string Name { get; set; }

    private ObservableCollection<Task> tasks =
        new ObservableCollection<Task>();
    public IList<Task> Tasks
    {
        get { return this.tasks; }
    }
}

The Task class just has one property - the name of the task.

The ProjectView class is a wrapper around the Project class (I got this idea from @timothymcgrath's answer). It keeps track of the name of a new task, and the current Project:

class ProjectView : INotifyPropertyChanged
{
    public Project Project { get; set; }

    private string newTaskName = string.Empty;
    public string NewTaskName
    {
        get { return this.newTaskName; }
        set
        {
            this.newTaskName = value;
            this.OnPropertyChanged("NewTaskName");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propName)
    {
        PropertyChangedEventHandler eh = this.PropertyChanged;
        if(null != eh)
        {
            eh(this, new PropertyChangedEventArgs(propName));
        }
    }
}

You'll need a new class that will be used as the DataContext. Something like this:

class Model
{
    private ObservableCollection<ProjectView> projects =
        new ObservableCollection<ProjectView>();
    public IList<ProjectView> Projects
    {
        get { return this.projects; }
    }
}

In the code behind, set the DataContext of the object to an instance of the above class:

public class Window1
{
    public Window1()
    {
        this.InitializeComponent();

        this.DataContext = this.model;
    }

    private Model model = new Model();
}

In the XAML, the bindings should be modified to bind to the above properties:

<ListBox x:Name="projectList" IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding Path=Projects}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Project.Name}" />
                <ListBox x:Name="taskList"
                    ItemsSource="{Binding Project.Tasks}"
                    DisplayMemberPath="Name" />
                <TextBox x:Name="textBoxTask"
                    Text="{Binding Path=NewTaskName, UpdateSourceTrigger=PropertyChanged}"/>
                <Button x:Name="ButtonAddNewTask" Content="Test"
                    Click="ButtonAddNewTask_Click" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Finally, in the click handler for the button, create the task. The DataContext of the Button will be the ProjectView for that item.

private void ButtonAddNewTask_Click(object sender, RoutedEventArgs e)
{
    Button btn = (Button)sender;
    ProjectView curProject = btn.DataContext as Project;
    if(null != curProject)
    {
        curProject.Project.Tasks.Add(new Task()
        {
            Name = curProject.NewTaskName
        });
    }
}

Since all of the controls get their values via binding, you don't need to access the control itself to get the data - just use the data structures that are supplying the controls already.

It would probably be better to move the code that creates the Task into another class (possibly Project), but I just left it in the event handler for ease of typing on my part.

Update 2: Modified the above code to move the NewTaskName property into a separate class that wraps an instance of Project for use with the UI. Does this work better for you?

Andy
Problem here is even if I get the datacontext, the project in this case I can't get hold of the Add new task textbox so when I add a task to the project I can't get the value for the tasks name from the textbox.
Emil C
You shouldn't need the TextBox, though. Instead, the DataContext should contain the name of the Project, which TextBox.Text is bound to.
Andy
But then how do I create a new Task when I press the button? I need to know whats inside the textbox.
Emil C
I really appreciate all the help I get. But it is not an ok solution. Because it is wiered to have the property on the project.
Emil C
The problem is, you need to store it somewhere. You can't just have one string member of your Window (or whatever), since there needs to be one per project. Is it required that you have the TextBox in the list, or would a separate dialog be ok?
Andy
Yes. It needs to be in the list. I have one outside the list as well.But I got i working now and it works fine now.
Emil C
+2  A: 

I'm assuming your Project ListBox is populated with an Collection of Project objects. I would add an AddNewTask ICommand to the Project class and expose it through a property. Then bind the Add New Task button to the new AddNewTask ICommand. For the CommandParameter, put the TaskName in and it will be passed into the command.

Try reading up on some MVVM (Model View ViewModel) for some examples of how this works. It is very clean and works great.

timothymcgrath
I will read up more on the Model ViewModel concept. But cluttering upp my objects with code concerning the UI feels like a really bad concept to me. So I want to avoid this as much as possible.
Emil C
So, what you should really do is wrap the real business object with a ViewModel object. It is basically an object wrapping the Project object, but it translates the Project object to work with the WPF UI. You would then put the Command on the Project ViewModel object.
timothymcgrath
A: 

This solution worked for the task at hand so to speak.

    private void ButtonAddNewTask_Click(object sender, RoutedEventArgs e)
    {
        Button button = (Button)sender;
        DependencyObject obj = LogicalTreeHelper.GetParent(button);
        StackPanel item = obj as StackPanel;
        TextBox textBox = item.FindName("textBoxTask") as TextBox;
        ListBox listBox = item.FindName("taskList") as ListBox;

        Project proj = button.DataContext as Project;
        if(proj.Tasks == null)
            proj.Tasks = new List<Task>();

        listBox.ItemsSource = proj.Tasks;
        listBox.Items.Refresh();
    }
Emil C
While that works, it isn't the best solution IMO. It's much better to interact with your model objects rather than pulling the data from the controls themselves.
Andy
I got the ModelView described for me today. I'm a webbforms developer and thats why I think a bit different. I'm trying your example.
Emil C
A: 

I want to transform a listbox to textbox when I'm clicking a checkbox. This is for addresses that if an address isn't in the list, someone can write another address.

trikalos