ForeverStudent
ForeverStudent

Reputation: 2537

Best practice: How to get custom user input in WPF

Please allow me to present a simplified version of my problem:

Lets say I have a main window called MainWindow in which I would like to display some Person objects in MainWindow. Now, in order to instantiate these Person objects I need a bunch of different fields such as name, age, profession, favourite food, etc...

Here is my solution:

I try to get all input fields and instantiate a Person in a secondary window and then send back the result to the main form.

MainWindow has a public method as follows:

public void (Person input) 
{
    // use the fields in input to add details to window
}

I have another window in the project called PersonInput that takes in its constructor a reference to a MainWindow and saves it in a field.

    private MainWindow owner;
    public PersonInput(MainWindow parent)
    {
        InitializeComponent();
        owner = parent;
    }

PersonInput has a number of input fields corresponding to the required fields of a Person object.

in addition it has a button called "AddPerson" with an associated onClick event handler as follows: (pseudoCode)

private void button_Click(object sender, RoutedEventArgs e)
{     
      //get all fields from this form..
      String enteredName = this.nameText.Text; 
      //get more fields....
      Person p = new Person(...);

      //owner is MainWindow, send Back the Person so details can be displayed
      owner.addPerson(p); 
      this.Close(); 
}

as you would expect, MainWindow has a Button named "AddPersonButton" which has an on click event handler like this:

private void button_Click(object sender, RoutedEventArgs e)
{
        PersonInput x = new PersonInput(this); //pass this as a reference
        //so this window can send us back the result when they have it

        x.Show(); //open the child window so user can enter information
}

While this method works, I am quite convinced it is not the best practice way to do it. I would like to learn the idiomatic .net WPF way of doing this. Please enlighten me

Upvotes: 0

Views: 2087

Answers (3)

PersonInput.xaml.cs

public class PersonInput : Window
{
    public void PersonInput()
    {
        InitializeComponent();
        Owner = Application.Current.MainWindow;
    }

    public static Person ShowDialog(Person initializer)
    {
        var vm = new PersonViewModel(initializer);
        var dlg = new PersonInput() { DataContext = vm };

        if (dlg.ShowDialog().GetValueOrDefault(false))
        {
            return vm.ToPerson();
        }

        return null;
    }

    private void OK_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = true;
    }

    private void Cancel_Click(object sender, RoutedEventArgs e)
    {
        DialogResult = false;
    }
}

PersonInputViewModel.cs

public class PersonViewModel : ViewModelBase
{
    public PersonViewModel(Person person = null)
    {
        if (person != null) 
        {
            //  Assuming Person has FirstName and LastName properties
            FirstName = person.FirstName;
            LastName = person.LastName;
            //  etc. etc. for all the rest
        }
    }

    public Person ToPerson()
    {
        return new Person()
        {
            FirstName = this.FirstName,
            LastName = this.LastName,
            //  etc. etc. for all other properties
        };
    }

    private string _firstName = null;
    public string FirstName {
        get { return _firstName; }
        set {
            if (value != _firstName) {
                _firstName = value;
                OnPropertyChanged(nameof(FirstName));
            }
        }
    }
}

ViewModelBase.cs

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

PersonInput.xaml

<Window xmlns:blahblahblah="Blah blah blah" etc etc
    Title="Person" 
    Height="640" 
    Width="480"
    ShowInTaskbar="False"
    ResizeMode="CanResizeWithGrip"
    WindowStartupLocation="CenterOwner" 
    >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="180" Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0">First Name</Label>
        <TextBox
            Grid.Row="0" 
            Grid.Column="1"
            Text="{Binding FirstName}"
            />

        <Label Grid.Row="1" Grid.Column="0">Last Name</Label>
        <TextBox
            Grid.Row="1" 
            Grid.Column="1"
            Text="{Binding LastName}"
            />

        <StackPanel 
            Orientation="Horizontal" 
            Grid.Column="1" 
            Grid.Row="10"
            HorizontalAlignment="Right"
            >
            <Button Content="_OK" Click="OK_Click" />
            <Button Content="_Cancel" Click="Cancel_Click" />
        </StackPanel>                
    </Grid>
</Window>

Upvotes: 3

Serg Suhanov
Serg Suhanov

Reputation: 31

Another use MVVM practice:

Solution structure

public class Person {
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

public class PersonListViewModel : DependencyObject {
    public ObservableCollection<Person> Items { get; set; }

    public Person CurrentPerson
    {
        get { return (Person)GetValue(CurrentPersonProperty); }
        set { SetValue(CurrentPersonProperty, value); }
    }
    public static readonly DependencyProperty CurrentPersonProperty = DependencyProperty.Register("CurrentPerson", typeof(Person), typeof(PersonListViewModel));

    public ICommand AddCommand { get; set; }
    public ICommand EditCommand { get; set; }

    public PersonListViewModel() {
        Items = new ObservableCollection<Person>();

        AddCommand = new RelayCommand(p=> add() );
        EditCommand = new RelayCommand(p=> { return CurrentPerson != null; }, p => edit());
    }

    private void add() {
        Person p= new Person();
        p.Id = Items.Count();
        p.Name = "New Name";
        p.Birthday = DateTime.Now;
        Items.Add(p);
    }

    private void edit() {
        var viewModel = new PersonItemViewModel(CurrentPerson);
        var view = new View.PersonEditWindow();
        view.DataContext = viewModel;
        view.Show(); 
    }
}

public class PersonItemViewModel : DependencyObject {
    Person person;

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }
    public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(PersonItemViewModel) );

    public DateTime Birthday
    {
        get { return (DateTime)GetValue(BirthdayProperty); }
        set { SetValue(BirthdayProperty, value); }
    }
    public static readonly DependencyProperty BirthdayProperty = DependencyProperty.Register("Birthday", typeof(DateTime), typeof(PersonItemViewModel));

    public PersonItemViewModel() {

    }
    public PersonItemViewModel(Person source) {
        this.person = source;

        Name =  person.Name;
        Birthday = person.Birthday;
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {
        base.OnPropertyChanged(e);

        if (e.Property == NameProperty) {
            person.Name = (string) e.NewValue;
        }

        if (e.Property == BirthdayProperty) {
            person.Birthday = (DateTime)e.NewValue;
        }
    }
}

List Form:

<Window x:Class="WpfApplication1.View.PersonListWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApplication1.View"
    mc:Ignorable="d"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    Title="PersonListWindow" Height="300" Width="300"
    xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
    d:DataContext="{d:DesignInstance Type=viewModel:PersonListViewModel, IsDesignTimeCreatable=True}"
    >

<Grid Margin="8">
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
    </Grid.RowDefinitions>

    <DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentPerson, Mode=TwoWay}">
    </DataGrid>

    <StackPanel Grid.Row="1" Height="32" Orientation="Horizontal" HorizontalAlignment="Center">
        <Button Margin="4" Command="{Binding AddCommand}">Add</Button>
        <Button Margin="4" Command="{Binding EditCommand}">Edit</Button>
    </StackPanel>
</Grid>

Edit Form:

<Window x:Class="WpfApplication1.View.PersonEditWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApplication1.View"
    mc:Ignorable="d"
    Title="PersonEditWindow" Height="300" Width="300"
            xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
    d:DataContext="{d:DesignInstance Type=viewModel:PersonItemViewModel, IsDesignTimeCreatable=True}"

    >
<Grid Margin="20">
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>


    <TextBlock Grid.Row="0">Name</TextBlock>
    <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Name}"></TextBox>

    <TextBlock Grid.Row="1">Birthday</TextBlock>
    <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Birthday}"></TextBox>

</Grid>

Results (Image)

Upvotes: 0

Silas Reinagel
Silas Reinagel

Reputation: 4203

The biggest weakness of this approach is the strong coupling between MainWindow and PersonInput.

A slightly better approach would be to use Observer Pattern and have the Main Window anonymously subscribe.

Simple solution example code:

public interface IAddPersonObserver
{
    void OnPersonAdded(Person person);
}

public interface IAddPersonObservable
{
    void Subscribe(IAddPersonObserver observer);
    void Unsubscribe(IAddPersonObserver observer);
}

public class MainWindow : IAddPersonObserver
{
    ...

    private void button_Click(object sender, RoutedEventArgs e)
    {
        PersonInput x = new PersonInput(); 
        x.Subscribe(this);
        x.Show();
    }

    public void OnPersonAdded(Person addedPerson)
    {
        addPerson(addedPerson); // or whatever view update code you want
    }
}

Further improvements would revolve around separating the MainWindow from knowing what view gets created or shown, and having an intermediary object (such as a PersonRepository) store/hold/provide the important business data. This is much better than having the application data actually live inside the Views and Application Windows.

Upvotes: 0

Related Questions