0

I have a form, which is binded to the model. There is one standard, basic model and few children models (with additional fields).

Above the model's controls there is a radio buttons group and upon selecting one of them the forementioned additional fields appear (in this case the sentence field).

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void sentencedPersonRadioButton_Checked(object sender, RoutedEventArgs e)
    {
        sentenceTextBox.Visibility = Visibility.Visible;
        DataContext = new SentencedPerson();
    }

    private void personRadioButton_Checked(object sender, RoutedEventArgs e)
    {
        sentenceTextBox.Visibility = Visibility.Hidden;
        DataContext = new Person();
    }
}

Lets say there is a Person and SentencedPerson:

public class Person: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(String propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private String name;
    public String Name
    {
        get
        {
            return name;
        }

        set
        {
            if (value == name)
            {
                return;
            }

            name = value;
            OnPropertyChanged("Name");
        }
    }
}

public class SentencedPerson : Person
{
    private String sentence;
    public String Sentence
    {
        get
        {
            return sentence;
        }

        set
        {
            if (value == sentence)
            {
                return;
            }

            sentence = value;
            OnPropertyChanged("Sentence");
        }
    }
}

What is the proper way to design such a connections? Adding new 'checked' event handlers feels so cumbersome... I heard about MVVM pattern in which there would be some kind of PersonContext with Person and SentencedPerson props inside. But it does not change the need of 'checked' events.

Also know there is a problem because the values of common fields are after setting the new DataContext.

y434y
  • 633
  • 1
  • 8
  • 27

2 Answers2

1

This is a quite broad question but I will give you some pointers.

MVVM is the recommended design pattern to use when building XAML based applications.

You could create a view model class with a "CurrentSelectedContent" property of type object or Person and an enum property that you bind the RadioButton to.

Please refer to the following link for more information and an example of how to bind a RadioButton to an enum source property using MVVM:

How to bind RadioButtons to an enum?

Once you have done this you could set the value of the "CurrentSelectedContent" property based on the radio button selection in the setter of the enum source property in the view model:

private MyLovelyEnum _enum;
public MyLovelyEnum VeryLovelyEnum
{
    get
    {
        return _enum;
    }
    set
    {
        _enum = value;
        switch (value)
        {
            case MyLovelyEnum.Person:
                CurrentSelectedContent = new Person();
                break;
            //...
        }
        OnPropertyChanged("VeryLovelyEnum");

    }
}

Make sure that the "CurrentSelectedContent" property raises the PropertyChanged event and that the view model class implements the INotifyPropertyChanged interface.

In the view you could then use a ContentControl and bind its Content property to the "CurrentSelectedContent" property:

    <ContentControl Content="{Binding Content}">
        <ContentControl.ContentTemplate>
            <DataTemplate>
                <TextBox Text="{Binding Name}" />
            </DataTemplate>
        </ContentControl.ContentTemplate>
    </ContentControl>

Also make sure that you set the DataContext of the view to an instance of your view model:

public MainWindow()
{
    InitializeComponent();
    DataContext = new ViewModel();
}

This is the rough idea on how to do this using the MVVM pattern. Instead of handling events in the code-behind of the view you bind to source properties and instead of setting the DataContext property of specific UI elements explicitly you bind the Content property of a ContentControl to an object that you create in the view model class.

Hope that helps.

Community
  • 1
  • 1
mm8
  • 163,881
  • 10
  • 57
  • 88
  • I don't think that I get this. I still have to define somewhere that if the enum is set to sentenced person. the sentence TextBox has to be displayed. Beside that I also see the problem with filling the DataContext view model. In MyLovelyEnum's enum setter I am loosing the data from the controls, I mean the common data from the base object. It described case the name property's value. – y434y Feb 20 '17 at 23:04
0

You just need One model:

public class Person : INotifyPropertyChanged
{
    string _name;
    public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }

    bool _isSentenced;
    public bool IsSentenced { get { return _isSentenced; } set { _isSentenced = value; RaisePropertyChanged("IsSentenced"); } }

    string _sentence;
    public string Sentence { get { return _sentence; } set { _sentence = value; RaisePropertyChanged("Sentence"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    } 
}

Use IsSentenced and bind the RadioButton to it. Also, check the Visibility of the TextBox displaying the Sentence string to the IsChecked property of the RadioButton, using a Visibility to Bool converter. Here is a simple example:

<Window.Resources>
    <local:VisibilityToBoolConverter x:Key="VisibilityToBoolConverter"/>
</Window.Resources>
<ListBox DataContext="{Binding}" ItemsSource="{Binding Persons}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBox Text="{Binding Name}" />
                <RadioButton Content="Is sentenced to death" IsChecked="{Binding IsSentenced}" />
                <DockPanel  Visibility="{Binding IsSentenced , Converter={StaticResource VisibilityToBoolConverter}}">
                    <Label Content="Sentence:  "/>
                    <TextBlock Text="{Binding Sentence}" />
                </DockPanel>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate> 
</ListBox>

in which

public class VisibilityToBoolConverter : IValueConverter
{ 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if ((bool)value == true)
            return Visibility.Visible;
        return Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if ((Visibility)value ==  Visibility.Visible)
            return true;
        return false;
    }
}

and the ViewModel is:

public class PersonViewModel : INotifyPropertyChanged
{
    public PersonViewModel()
    {
        Person m1 = new Person() { Name = "person 1", IsSentenced = false, Sentence = "S S S" };
        Person m2 = new Person() { Name = "person 2", IsSentenced = false, Sentence = "B B B" };
        Person m3 = new Person() { Name = "person 3", IsSentenced = true, Sentence = "F F F" };
        _persons = new ObservableCollection<Person>() { m1, m2, m3 }; 

    } 
    ObservableCollection<Person> _persons;
    public ObservableCollection<Person> Persons { get { return _persons; } set { _persons = value; RaisePropertyChanged("Persons"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
}

your main window should set the DataContext:

    public MainWindow()
    { 
        PersonViewModel mv = new PersonViewModel();
        this.DataContext = mv;
        InitializeComponent(); 
    }

Edit

If there are many states for a person, ComboBox is a more natural choice. You should have an Enum that describes the states:

public enum MyTypes
{
    None,
    IsA,
    IsB,
    IsC
}

and the Person should have a peroperty that shows the state:

public class Person : INotifyPropertyChanged
{
    MyTypes _thetype;
    public MyTypes TheType { get { return _thetype; } set { _thetype = value; RaisePropertyChanged("TheType"); } }

    string _name;
    public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
}

Since you need to bind the ItemsSource of your ComboBox to a list of states, one possibility is to adjust the ViewModel to have such a list:

public class PersonViewModel : INotifyPropertyChanged
{
    public PersonViewModel()
    {
        Person m0 = new Person() { Name = "person 1", TheType = MyTypes.None };
        Person m1 = new Person() { Name = "person 1", TheType =  MyTypes.IsA };
        Person m2 = new Person() { Name = "person 2", TheType = MyTypes.IsB };
        Person m3 = new Person() { Name = "person 3", TheType = MyTypes.IsC };
        _persons = new ObservableCollection<Person>() { m0, m1, m2, m3 };

        _types = Enum.GetNames(typeof(MyTypes)).ToList();
    }

    List<string> _types;
    public List<string> Types { get { return _types; } } 


    ObservableCollection<Person> _persons;
    public ObservableCollection<Person> Persons { get { return _persons; } set { _persons = value; RaisePropertyChanged("Persons"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
}

and at last, the View:

<Window.Resources>
    <local:EnumToSentenceConverterx:Key="EnumToSentenceConverter"/>
    <local:NoneToCollapsedConverter x:Key="NoneToCollapsedConverter"/>
    <local:EnumToStringConverter x:Key="EnumToStringConverter"/>
</Window.Resources>
<ListBox Name="lb" DataContext="{Binding}" ItemsSource="{Binding Persons}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBox Text="{Binding Name}" />
                <ComboBox Name="cb" ItemsSource="{Binding ElementName=lb, Path=DataContext.Types}" SelectedValue="{Binding TheType, Mode=TwoWay, Converter={StaticResource EnumToStringConverter}}" />
                <DockPanel  Visibility="{Binding ElementName=cb, Path=SelectedValue, Converter={StaticResource NoneToCollapsedConverter}}">
                    <Label Content="Sentence:  " DockPanel.Dock="Left"/>
                    <TextBlock Text="{Binding TheType, Converter={StaticResource EnumToStringConverter}}" DockPanel.Dock="Right" VerticalAlignment="Center" />
                </DockPanel>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Note that you need three converters. One sets the Visibility of The Sentence part to Collapsed, it the type is None:

public class NoneToCollapsedConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.ToString() == "None")
            return Visibility.Collapsed;
        return Visibility.Visible;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The two others are self descriptive:

public class EnumToStringConverter : IValueConverter
{ 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value.ToString();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Enum.Parse(typeof(MyTypes), value.ToString());
    }
}

and

public class EnumToSentenceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        switch ((MyTypes)value)
        {
            case MyTypes.IsA:

                break;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Hope it helps.

rmojab63
  • 3,513
  • 1
  • 15
  • 28
  • Your idea with boolean value IsSentenced is quite interesting, but what if there are fifty of Person's subclasses with different properties. There would be fifty IsXYZ methods? – y434y Feb 20 '17 at 23:08