12
\$\begingroup\$

I am creating a C# WPF application. I am intending to bind all of the GUI controls via data binding to my ViewModel, rather than code behind - as per MVVM.

I have made some good headway with this, but unsure about something.

In my ViewModel, I am binding all the properties like this:

 private List<string> listBoxCodec;
    public List<string> ListBoxCodec
    {
        get
        {
            return this.listBoxCodec;
        }
        set
        {
            this.listBoxCodec = value;
            OnPropertyChanged("ListBoxCodec");
        }
    }

and like this:

private string create_FormatSelectedItem;
    public string Create_FormatSelectedItem
    {
        get { return create_FormatSelectedItem; }
        set
        {
            create_FormatSelectedItem = value;
            OnPropertyChanged("Create_FormatSelectedItem");
        }
    }

To my XAML like this:

 <ComboBox x:Name="lstBoxCodec" ItemsSource="{Binding ListBoxCodec}" SelectedItem="{Binding Path=Create_SelectedListBoxCodecsItem, Mode=TwoWay}">

etc...

My GUI has perhaps 50-100 controls/input fields (Text boxes, combo-boxes, etc.) so using the above code to bind these controls is adding up to thousands of lines of code in my ViewModel.

Is there a more efficient way to bind these controls, or is this pretty much it?

EDIT: Example of the GUI to help with a comment question: enter image description here

Some example XAML: (Output)

<Grid HorizontalAlignment="Stretch"  VerticalAlignment="Stretch" Grid.RowSpan="2" Grid.Column="1">
                            <TextBlock HorizontalAlignment="Left" Margin="18,12,0,0" TextWrapping="Wrap" Text="Summary" VerticalAlignment="Top" FontSize="20" Foreground="#FFED8500" FontFamily="Segoe WP Semibold" FontWeight="Bold"/>
                            <TextBlock Name="txtJobIntro" HorizontalAlignment="Left" Margin="18,74,0,0" TextWrapping="Wrap" Text="Job information will appear here as you create a new job..." VerticalAlignment="Top" FontWeight="Normal" FontSize="12" Foreground="#FF8E8E8E"/>
                            <!--Job summary-->
                            <ScrollViewer Name="CreateSummaryBox" HorizontalAlignment="Stretch"  Margin="10,44,0,0" VerticalAlignment="Stretch" Background="#FF505050"  Visibility="Collapsed">
                                <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" TextElement.FontSize="13" TextElement.FontWeight="Normal">
                                    <StackPanel>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,10,0" TextWrapping="Wrap" Text="{Binding Heading, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" FontSize="15" Foreground="#FFDE7900"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,5,0,5" TextWrapping="Wrap" Text="{Binding CreateInputFile, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" FontSize="12" Foreground="#FFD8D8D8"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding Format, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding FormatProfile, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding FPS, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding Res, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding Ratio, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding Duration, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding FileCount, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>

                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding A_Type, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding A_Depth, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding A_Samples, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding A_Channels, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding A_ChannelPositions, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <!--Effects-->
                                        <TextBlock HorizontalAlignment="Left" Margin="10,10,5,0" TextWrapping="Wrap" Text="Processing Options" VerticalAlignment="Top" FontSize="15" Foreground="#FFDE7900"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateProcessOptions, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <!--Output file-->
                                        <TextBlock HorizontalAlignment="Left" Margin="10,10,5,0" TextWrapping="Wrap" Text="Export File" VerticalAlignment="Top" FontSize="15" Foreground="#FFDE7900"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputExportType, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputExportFile, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <StackPanel FlowDirection="LeftToRight">
                                            <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputVideoFormat, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                            <TextBlock Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputVideoType, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        </StackPanel>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputX, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputFPS, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <!--<TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputScan, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>-->
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputBitDepth, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputAudioType, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputAudioBits, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputAudioChannels, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputAudioRetime, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>

                                        <TextBlock HorizontalAlignment="Left" Margin="10,0,0,0" TextWrapping="Wrap" Text="{Binding CreateOutputExportName, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>
                                    </StackPanel>
                                </Grid>
                            </ScrollViewer>
                        </Grid>

And (User Input)

<Expander  Header="Processing" HorizontalAlignment="Stretch" Margin="23,10,0.2,0" VerticalAlignment="Top" FontSize="20" Foreground="#FFED8500" Background="#FF585858" FontWeight="SemiBold" Name="exp2">
                                        <Grid  Height="200" HorizontalAlignment="Stretch" TextElement.Foreground="#FFCDCDCD" TextElement.FontSize="13" TextElement.FontFamily="Segoe UI">
                                            <TextBlock HorizontalAlignment="Left" Margin="660,170,-400.2,0" TextWrapping="Wrap" Text="Add upto three processes. List of processes will add to the summary box -&gt;" VerticalAlignment="Top" FontSize="13" Width="457"/>
                                            <TextBlock HorizontalAlignment="Left" Margin="25,9,0,0" TextWrapping="Wrap" Text="Choose Process:" VerticalAlignment="Top"  />
                                            <Button Content="Add Process" HorizontalAlignment="Left" Margin="530,168,0,0" VerticalAlignment="Top" Width="113" Command="{Binding Create_AddEffectCommand}" />
                                            <ComboBox ItemsSource="{Binding EffectCategories}" HorizontalAlignment="Left" Margin="23,28,0,0" VerticalAlignment="Top" Width="195" Height="23" Name="CreateChooseEffectCategory"  SelectedItem="{Binding EffectCategorySelectedItem}" SelectedIndex="2" SelectedValue="Standards Conversion" >
                                                <i:Interaction.Triggers>
                                                    <i:EventTrigger EventName="SelectionChanged">
                                                        <i:InvokeCommandAction Command="{Binding Create_ShowEffectsCommand}"
                                                      CommandParameter="{Binding YourCommandParameter}" />
                                                    </i:EventTrigger>
                                                </i:Interaction.Triggers>
                                            </ComboBox>
                                            <TextBlock HorizontalAlignment="Left" Margin="242,9,0,0" TextWrapping="Wrap" Text="Type:" VerticalAlignment="Top" />
                                            <ListBox HorizontalAlignment="Left" Height="113" Margin="242,27,0,0" VerticalAlignment="Top" Width="258"  BorderBrush="{x:Null}" FontWeight="SemiBold" Name="CreateLstBoxEffects" ItemsSource="{Binding ListBoxEffects}" SelectedItem="{Binding SelectedListBoxEffectsItem}"  IsEnabled="{Binding EffectsListBoxIsEnabled}" />
                                            <Button Content="Preview..." HorizontalAlignment="Left" Margin="25,166,0,0" VerticalAlignment="Top" Width="113" Command="{Binding OpenPreviewCommand}"/>
                                            <TextBlock HorizontalAlignment="Left" Margin="526,8,0,0" TextWrapping="Wrap" Text="Settings:" VerticalAlignment="Top" />
                                            <ComboBox HorizontalAlignment="Left" Margin="526,27,0,0" VerticalAlignment="Top" Width="113" Height="20" x:Name="CreateChooseEffecSetting" SelectedIndex="0">
                                                <ComboBoxItem>Default</ComboBoxItem>
                                            </ComboBox>
                                        </Grid>
                                    </Expander>
\$\endgroup\$
  • 2
    \$\begingroup\$ Do you really have 50-100 controls/inputs in a single view? \$\endgroup\$ – RobH Apr 22 '15 at 14:17
  • 2
    \$\begingroup\$ You can cut down on some of the boilerplate by having a base class for your view models as I alluded to here: codereview.stackexchange.com/a/13849/6172 \$\endgroup\$ – Jesse C. Slicer Apr 22 '15 at 14:37
  • 1
    \$\begingroup\$ @Mat'sMug : true - this case looks like the ubiquitous SE copy paste error of extra indentation on every line except the first. I'll leave the whitespace alone ;) \$\endgroup\$ – RobH Apr 22 '15 at 15:09
  • 2
    \$\begingroup\$ Thats correct, I dont really have that much indentation :) \$\endgroup\$ – Dan Sewell Apr 22 '15 at 15:12
  • 3
    \$\begingroup\$ You should not use List<T> with MVVM/WPF. You should use something that implements INotifyCollectionChanged, like ObservableCollection<T>. \$\endgroup\$ – Aron Apr 22 '15 at 17:41
20
\$\begingroup\$

Unlearn WinForms.

Your view has way too many concerns.

As @TeaDrivenDev mentioned, XAML is pretty verbose... and WPF bindings will turn your view model into a very combersome mess of getters and notifying setters... if you think in WinForms.

Dragging-and-dropping 100-some controls onto a UserForm creates ugly bloated code-behind in WinForms; there's no magic in WPF (well there is, actually), having a view with 100-some controls in WPF using databindings is bound (!) to create an ugly bloated view model class.


Fine. What then?

Let's look at this UI.

stack panel with expander controls

Forget about the details, look at the high-level picture. What do you see? I see a StackPanel with a number of Expander controls. Nothing unmanageable here.

That's all your Window should care about.

You need to zoom out and stop seeing all the controls you want to see on that window, and break it down into reusable components.

I bet each expander has the same properties here:

<Expander Header="Processing" 
          HorizontalAlignment="Stretch"
          Margin="23,10,0.2,0" 
          VerticalAlignment="Top" 
          FontSize="20" 
          Foreground="#FFED8500" 
          Background="#FF585858" 
          FontWeight="SemiBold" 
          Name="exp2">

You probably don't need a Name for them. And if you do, you probably need an x:Name instead. But you probably don't. I rarely need to name controls in WPF. So the only thing that differs between each section here, is the Header text and the actual content. Right? Using a Style here would reduce the markup and make it semantically clearer that these UI elements should look the same.

Why not make a separate user control for each section, each with its own view model, dealing with its own data bindings?

\$\endgroup\$
  • \$\begingroup\$ Thanks. I am actually starting to move to styles, and already have reduced the code alot, so that's good :) The user control idea is good. I have already done that with my 'Settings' tab (one of the tab on the main view, so should be able to do it with the rest. \$\endgroup\$ – Dan Sewell Apr 22 '15 at 14:58
  • \$\begingroup\$ The question/review was more on if/how to reduce the C# code, but the XAML stuff is all useful too :) \$\endgroup\$ – Dan Sewell Apr 22 '15 at 15:02
  • 1
    \$\begingroup\$ @DanSewell The C# code needs to go somewhere. Properly organizing your XAML is the starting point for cleaning up your view model: by breaking down the XAML into 5-6 files (assuming 4-5 expandable sections), you break down your gigantic view model into 5-6 much smaller, cleaner and specialized classes. WPF is all about the markup! \$\endgroup\$ – Mathieu Guindon Apr 22 '15 at 15:05
  • 3
    \$\begingroup\$ This is exactly the answer I was hoping someone would post! \$\endgroup\$ – RobH Apr 22 '15 at 15:10
  • \$\begingroup\$ Actually one more question, part of the reason for the MVVM, is to enable unit testing. Separating it into several user controls, will it make unit testing any harder? \$\endgroup\$ – Dan Sewell Apr 22 '15 at 16:48
11
\$\begingroup\$

After reading some of the comments and looking at a few things I think the underlying question is still unanswered. I'll break this answer up into 2 sections. One unit testing, and two smaller code.

Unit testing is TONS easier to do with switching to MVVM. The view model already needs to be public, as well as all the properties. As you progress in your learning of how MVVM works you'll start to see that there is ALMOST nothing that you can't test. For instance you can fire your ICommands in a unit test and assert that the state is all correct.

Since I can't see much of your control I will go off of what I can see. Lets say that you have some validation in place that won't let you export until you have the export file name and export location set. You could test manually.. or you can write your test something like this

//Wrote this test in Notepad++, so spelling might be slightly off
[Test]
public void WhenExportLocationAndFileNameAreSetExportButtonIsEnabled()
{
    var viewModel = new ExportVideoViewModel();
    Assert.That(viewModel.ExportFileCommand.CanExecute(), Is.False);

    viewModel.ExportLocation = @"C:\SomeValidLocation\";
    Assert.That(viewModel.ExportFileCommand.CanExecute(), Is.False);

    viewModel.ExportFileName = "AValidName";
    Assert.That(viewModel.ExportFileName, Is.EqualTo("AValidName.mxf"));

    Assert.That(viewModel.ExportFileCommand.CanExecute(), Is.True);
}

This is where a person can start seeing the full power of MVVM. With only the viewmodel I can now test how it flows, and works.

As for the smaller code in your view model there are a few ways around this. You could use a framework (I've never used one, so I can't speak to how good they are) for MVVM that helps with the getters or setters. I typically make my own BaseViewModel with a Get and a Set that uses an Expression. Since it is an expression I can programatically get the name of the property. As of C# 6 you can use nameof. And there is even a attribute that you can use in I think .NET 4.5 (quotation needed) that will set the name of the property for you.

Anywho using an expression my ViewModel properties turn into something like this

public class FoobarViewModel : BaseViewModel
{
    public string ExportFileName { get { return Get(() => ExportFileName); } set { Set(() => ExportFileName, value); } }
    public ICommand CopyExportFileNameCommand { get { return Get(() => CopySearchStringCommand); } }

    public FoobarViewModel()
    {
        AddNewProperty(() => ExportFileName, IsExportFileNameValid);
        AddNewProperty(() => CopyExportFileNameCommand, new ActionCommand(CopyExportFileName));
    }

    private bool IsExportFileNameValid(string newValue)
    {
        return false;
    }

    private void CopyExportFileName(object x)
    {
        System.Windows.Clipboard.SetText(ExportFileName);
    }
}

it isn't the biggest saver on lines of code, but it saves a few here and there. Add it up with what was suggested in another answer and you'll get some seriously smaller classes.

Hope this helps some.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.