0

I am new to the MVVM pattern and things are coming to me ever so slowly, I want to be able to click a button on my form and then it dynamically create a textbox at runtime. I have a 'Add Title' and also 'Add Question' which both add textboxes but at different locations, you can add as many questions under one title. I have Created a class called Standard in this class it holds:

public class Standard
{
    string _title;
    ObservableCollection<string> _questions;
    public event PropertyChangedEventHandler PropertyChanged;

  #region NofiftyPropChnage
  protected void NotifyOfPropertyChanged(string name)
  {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
          handler(this, new PropertyChangedEventArgs(name));
      }
  }

  protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
  {
      NotifyOfPropertyChanged(property.GetMemberInfo().Name);
  }
  #endregion

  #region Properties
  public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            NotifyOfPropertyChanged(() => Title);
        }
    }

    public ObservableCollection<string> Questions
    {
        get { return _questions; }
        set
        {
            _questions = value;
            NotifyOfPropertyChanged(() => Questions);
        }
    }
  #endregion
}

This class holds a Title property and also a list of Questions property because you can add Questions under a Title.

I also have a ViewModel class which holds:

class ViewModel :INotifyPropertyChanged
{
    #region NotifyPropertyChange
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyOfPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
}

protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
{
        NotifyOfPropertyChanged(property.GetMemberInfo().Name);
}
    #endregion

private ObservableCollection<Standard> _standardCollection;
public ObservableCollection<Standard> StandardCollection
{
        get
        {
            return _standardCollection;
        }
        set
        {
            _standardCollection = value;
            NotifyOfPropertyChanged(() => StandardCollection);
        }
}
}

This class holds a list of standards, a standard is when you click save with the text boxes and information in the text boxes done. It saves as a Standard

Finally my XAML code:

<Grid>

<button Content="Add Title"/>
<button Content="Add Question"/>

<StackPanel>
        <ItemsControl ItemsSource="{Binding StandardCollection}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:Standard}">
                    <Grid>
                        <TextBox Text="{Binding Title}"/>
                        <ItemsControl ItemsSource="{Binding Questions}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <TextBox Text="{Binding Questions}"/>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
</StackPanel>


</Grid>

Everything runs and there are no errors but when I click 'Add Title' or 'Add Question' no textbox appears, any help?

user3157821
  • 119
  • 1
  • 13

5 Answers5

1

Standard needs to implement the INotifyPropertyChanged interface. Generally you shouldn't do this more than once though, just declare one base class that implements that stuff and inherit all your view models from that. Also if you use package manager to add MVVM Lite to your project then you'll get a lot of this stuff provided for you.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Thanks for your answer, I added `INotifyPropertyChanged ` to my standard class but nothing changes, what do i add to my buttons in XAML? – user3157821 Jan 07 '14 at 10:09
  • Yes, you need to add an ICommand property to your view model and bind your Buttons "Command" property to it, there's more info about in [this SO question](http://stackoverflow.com/questions/862570/how-can-i-use-the-relaycommand-in-wpf). – Mark Feldman Jan 07 '14 at 10:15
  • @MarkFeldman I can see they have used it to save, but i need it to add a text box? – user3157821 Jan 07 '14 at 10:17
  • When you add a RelayCommand to your ViewModel you provide a handler that gets called when the user presses the button (assuming it's Command handler has been bound to it). That handler should then do something to the view model e.g. adding an element to an ObservableCollection. If your ItemsControl is creating items such as TextBoxes from that ObservableCollection then they'll appear in the View. Having looked at it a bit close though there are multiple things wrong with this code... – Mark Feldman Jan 07 '14 at 10:24
  • ...e.g. you're setting the list ItemsSource to the Questions collection but then the TextBox inside each element is binding to it as well. I'll try to whip up an example that does what I think it is you're trying to do. – Mark Feldman Jan 07 '14 at 10:25
1

I have no idea why these other guys are banging on about the INotifyPropertyChanged interface, as that has so very little to do with ICommand, although it does appear that you have tried to use it without adding it to the Standard class definition.

Either way, it sounds to me like you need to use the RelayCommand, or similar. This is a class that extends the ICommand interface... you can think of it as a delegate command. Instead of defining a separate class for each command, you can simply define the command logic and the canExecute handler inline. Here is a simplified example:

public ICommand SaveCommand
{
    get { return new RelayCommand(execute => Save(), canExecute => CanSave()); }
}

...

<Button Content="Save" Command="{Binding SaveCommand}" />

You can find an implementation of it in the RelayCommand.cs page on GitHub and a description of it in the Commands, RelayCommands and EventToCommand page on MDSN Magazine.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • Thanks for your answer, I will need this for the save button but what about for the Add Title and Add Questions buttons. When i click they are not showing the Text Boxes? – user3157821 Jan 07 '14 at 10:14
  • 2
    Steady up Sheridan, sometimes people write a quick answer pointing out the stuff that immediately springs to mind and then follow it up with an edit later once they've had a chance to go over the question in more detail. Learn some patience...a little humility probably wouldn't go astray either. – Mark Feldman Jan 07 '14 at 10:18
  • @user3157821, what?? Try changing the word *Save* above into *AddQuestion* and you're half way there. You do know that there is no default `Save` and `CanSave` methods, don't you? This was just a demonstration of how you can call ordinary methods from a `RelayCommand`. So just create an `AddQuestion` method and call that from the `RelayCommand` instead. Read the linked page for full explanation of how it works. – Sheridan Jan 07 '14 at 10:56
  • 1
    @MarkFeldman... you said *sometimes people write a quick answer pointing out the stuff that immediately springs to mind and then follow it up with an edit later*... maybe you shouldn't be so desperate to earn reputation then and think before you write? How was I or any other user to know that that wasn't your full answer? You're responsible for what you write and if you write an answer that does not answer the question, then don't be surprised if you get negative comments or down voted for it... which *I* didn't do by the way. – Sheridan Jan 07 '14 at 10:59
  • 1
    @Sheridan http://blog.stackoverflow.com/2012/07/kicking-off-the-summer-of-love/ (Incidentally the code had a lot more problems that just the stuff you pointed out, if you've spent half as much time trying to help the guy out as you did trashing the rest of us who were then you would have realized that). – Mark Feldman Jan 07 '14 at 11:32
  • @MarkFeldman, *you* posted a very poor answer that was did not answer the question even slightly. I didn't down vote it (although I probably should have). I just pointed out that it didn't answer the question. If you don't like people telling you that your answer does not answer the question, then try actually answering the question rather than pathetically trying to find fault with my answer in retaliation. My answer may well have not pointed out every problem that this user has, but no part of it is incorrect. Now I think we've both wasted enough time on this, so can we move on now please? – Sheridan Jan 07 '14 at 11:48
1

Ok, I'll have another shot at this one. I've stripped out the Title part and just concentrated on the Questions in order to keep this as a minimal example. First you'll need a base class that implements INotifyPropertyChanged for your view models:

public abstract class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpresion)
    {
        var property = (MemberExpression)propertyExpresion.Body;
        this.OnPropertyChanged(property.Member.Name);
    }

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Next you'll need a class that implements ICommand for your buttons to bind to which causes handlers to get called when those buttons are pressed:

// by Josh Smith, http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members
}

Those two classes were written by others, if you add MVVM Lite project to your project you'll get them provided for you.

Next we need to create a view model with an ObservableCollection of Questions and a handler that gets called when the user presses the button:

public class MyViewModel : ObservableObject
{
    public ICommand AddQuestionCommand {get; private set;}

    ObservableCollection<string> _questions = new ObservableCollection<string>();
    public ObservableCollection<string> Questions
    {
        get { return _questions; }
        set
        {
            _questions = value;
            OnPropertyChanged(() => Questions);
        }
    }

    public MyViewModel()
    {
        this.AddQuestionCommand = new RelayCommand(new Action<object>((o) => OnAddQuestion()));
    }

    private void OnAddQuestion()
    {
        this.Questions.Add("new item");
    }

}

Obviously you'll need to create an instance of this and set it as your window's DataContext. When the command gets triggerd the handler gets called and it in turn adds a new string to the collection. The XAML now needs to bind a button to that command and use the Questions collection to create a list of TextBlocks that display them all:

<StackPanel>
    <Button Content="Add Question" Command="{Binding AddQuestionCommand}" HorizontalAlignment="Left"/>
    <ItemsControl ItemsSource="{Binding Questions}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBox Text="{Binding .}" Width="200" HorizontalAlignment="Left"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

Hopefully this should give you a starting point. If I've missed something or you need clarification on anything then pls post a follow-up and I'll do my best.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Very Detailed answer! Thank you. Firstly, when I click Add Question all I want to appear is a 'Text Box' no writing. Secondly, When I click 'Add Question' it appears at the very bottom of the page and not at the top of the Stackpanel? – user3157821 Jan 07 '14 at 11:02
  • I've updated the XAML to include a template with an EditBox. You still need the collection of strings because every element in the listbox needs a corresponding element in the collection, if you don't want any text to appear then just replace "new item" with an empty string. Not sure what's causing your bottom page alignment, have you placed this XAML inside something else that may be causing it? Try making it the only thing in the window. – Mark Feldman Jan 07 '14 at 11:07
  • 1
    Thanks for your help and code, all worked fine and I figured out how to move the text boxes. Correct Answer. – user3157821 Jan 07 '14 at 11:19
  • On The XAML, When I'm adding 'Title' how do I add it into the same Stackpanal? – user3157821 Jan 07 '14 at 12:25
1

You will need to change your code heavily to make it work. Do the following:

Step 1. Add Class RelayCommand:

public class RelayCommand : ICommand
{
    public Func<bool> CanExecute { get; set; }
    public Action Execute { get; set; }

    public RelayCommand()
    {
    }

    public RelayCommand(Action execute)
    {
        Execute = execute;
    }

    #region ICommand Members

    bool ICommand.CanExecute(object parameter)
    {
        if (this.CanExecute == null)
        {
            return true;
        }
        else
        {
            return this.CanExecute();
        }
    }

    event EventHandler ICommand.CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    void ICommand.Execute(object parameter)
    {
        this.Execute();
    }

    #endregion
}

Step 2. Add Commands in ViewModel

public ICommand AddTitle { get; private set; }
public ICommand AddQuestion { get; private set; }

public ViewModel()
{
    _standardCollection = new ObservableCollection<Standard>();

    AddTitle = new RelayCommand(OnAddTitle);
    AddQuestion = new RelayCommand(OnAddQuestion);
}

void OnAddTitle()
{
    _standardCollection.Add(new Standard());
}

void OnAddQuestion()
{
    _standardCollection.Last().Questions.Add(new Question("Some Question"));
}

Step 3. Bind buttons

<Button Content="Add Title"  Command="{Binding AddTitle}"/>
<Button Content="Add Question" Command="{Binding AddQuestion}"/>

You will also have to fix you layount in XAML. Since the user can change the question text, you should create a separate class Question.

Usman Zafar
  • 1,919
  • 1
  • 15
  • 11
0

Try implementing INotifyPropertyChanged on class Standard.

public class Standard : INotifyPropertyChanged
{
    string _title;
    ObservableCollection<string> _questions;
    public event PropertyChangedEventHandler PropertyChanged;

  #region NofiftyPropChnage
  protected void NotifyOfPropertyChanged(string name)
  {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
      {
          handler(this, new PropertyChangedEventArgs(name));
      }
  }

  protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
  {
      NotifyOfPropertyChanged(property.GetMemberInfo().Name);
  }
  #endregion

  #region Properties
  public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            NotifyOfPropertyChanged(() => Title);
        }
    }

    public ObservableCollection<string> Questions
    {
        get { return _questions; }
        set
        {
            _questions = value;
            NotifyOfPropertyChanged(() => Questions);
        }
    }
  #endregion
}
Faisal
  • 4,054
  • 6
  • 34
  • 55