5

I'm trying to choose the best way to implement this UI in MVVM manner. I'm new to WPF (like 2 month's) but I have huge WinForms experience. enter image description here

The ListBox here act's like a TabControl (so it switches the view to the right), and contains basically the Type of item's displayed in tables. All UI is dynamic (ListBox items, TabItems and Columns are determined during run-time). The application is targeting WPF and Silverlight.

Classes we need for ViewModel:

public abstract class ViewModel : INotifyPropertyChanged {}
public abstract class ContainerViewModel : ViewModel
{
    public IList<ViewModel> Workspaces {get;set;}
    public ViewModel ActiveWorkspace {get;set;}
}
public class ListViewModel<TItem> where TItem : class
{
    public IList<TItem> ItemList { get; set; }
    public TItem ActiveItem { get; set; }
    public IList<TItem> SelectedItems { get; set; }
}
public class TableViewModel<TItem> : ListViewModel<TItem> where TItem : class
{
    public Ilist<ColumnDescription> ColumnList { get; set; }
}

Now the question is how to wire this to View.

There are 2 base approaches I can see here:

  • With XAML: due to lack of Generics support in XAML, I will lose strong typing.
  • Without XAML: I can reuse same ListView<T> : UserControl.

Next, how to wire data, I see 3 methods here (with XAML or without doesn't matter here). As there is no simple DataBinding to DataGrid's Columns or TabControl's TabItems the methods I see, are:

  • Use DataBinding with IValueConverter: I think this will not work with WPF|Silverlight out of the box control's, as some properties I need are read-only or unbindable in duplex way. (I'm not sure about this, but I feel like it will not work).
  • Use manual logic by subscribing to INotifyPropertyChanged in View: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....

  • Use custom controll's that support this binding: Code by myself or find 3d party controls that support this binding's (I don't like this option, my WPF skill is too low to code this myself, and I doubt I will find free controls)


Update: 28.02.2011 Things get worser and worser, I decided to use TreeView instead of ListBox, and it was a nightmare. As you probably guess TreeView.SelectedItems is a readonly property so no data binding for it. Ummm all right, let's do it the old way and subscribe to event's to sync view with viewmodel. At this point a suddenly discovered that DisplayMemberPath does nothing for TreeView (ummmm all right let's make it old way ToString()). Then in View's method I try to sync ViewModel.SelectedItem with TreeView's:

private void UpdateTreeViewSelectedItem()
{
    //uiCategorySelector.SelectedItem = ReadOnly....

    //((TreeViewItem) uiCategorySelector.Items[uiCategorySelector.Items.IndexOf(Model.ActiveCategory)]).IsSelected = true;
    // Will not work Items's are not TreeViewItem but Category object......

    //((TreeViewItem) uiCategorySelector.ItemContainerGenerator.ContainerFromItem(Model.ActiveCategory)).IsSelected = true;
    //Doesn't work too.... NULL // Changind DataContext=Model and Model = new MainViewModel line order doesn't matter.
    //Allright.. figure this out later...
}

And none of methods I was able to think of worked....

And here is the link to my sample project demonstrating Control Library Hell with MVVM: http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip

Alex Burtsev
  • 12,418
  • 8
  • 60
  • 87

4 Answers4

1

Maciek's answer is actually even more complicated than it needs to be. You don't need template selectors at all. To create a heterogeneous tab control:

  1. Create a view model class for each type of view that you want to appear as tab items. Make sure each class implements a Text property that contains the text that you want to appear in the tab for its item.

  2. Create a DataTemplate for each view model class, with DataType set to the class's type, and put the template in the resource dictionary.

  3. Populate a collection with instances of your view models.

  4. Create a TabControl and bind its ItemsSource to this collection, and add an ItemTemplate that displays the Text property for each item.

Here's an example that doesn't use view models (and that doesn't implement a Text property either, because the objects I'm binding to are simple CLR types), but shows how template selection works in this context:

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:sys="clr-namespace:System;assembly=mscorlib"
  xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib">
  <DockPanel>  
   <DockPanel.Resources>
        <coll:ArrayList x:Key="Data">
          <sys:String>This is a string.</sys:String>
          <sys:Int32>12345</sys:Int32>
          <sys:Decimal>23456.78</sys:Decimal>
        </coll:ArrayList>
        <DataTemplate DataType="{x:Type sys:String}">
          <TextBlock Text="{Binding}"/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type sys:Int32}">
          <StackPanel Orientation="Horizontal">
           <TextBlock>This is an Int32:</TextBlock>
           <TextBlock Text="{Binding}"/>
          </StackPanel>
        </DataTemplate>
        <DataTemplate DataType="{x:Type sys:Decimal}">
          <StackPanel Orientation="Horizontal">
           <TextBlock>This is a Decimal: </TextBlock>
           <TextBlock Text="{Binding}"/>
          </StackPanel>
        </DataTemplate>
   </DockPanel.Resources>
    <TabControl ItemsSource="{StaticResource Data}">  
      <TabControl.ItemTemplate>
       <DataTemplate>
        <TextBlock Text="{Binding}"/>
       </DataTemplate>
      </TabControl.ItemTemplate>
    </TabControl>
  </DockPanel>
</Page>

Of course in a real MVVM application those DataTemplates would use UserControls to map each type to its view:

<DataTemplate DataType="{x:Type my:ViewModel}">
   <my:View DataContext="{Binding}"/>
</DataTemplate>
Robert Rossney
  • 94,622
  • 24
  • 146
  • 218
  • Thank you for your reply about TabControl, but I'm more interested on: binding DataGrid's columns, sort column, DataGrid's selected rows. – Alex Burtsev Feb 27 '11 at 20:57
  • This will not work is Silverlight (I'm trargeting both WPF and Silverlight), Maciek is right, you need iether use IValueConverter or derive from TabItem. – Alex Burtsev Feb 27 '11 at 21:37
  • Ah, I didn't see that Silverlight's necessary. That does change things. Better template selection would really improve SL. – Robert Rossney Feb 27 '11 at 23:29
1

Maciek and Robert already gave you some ideas on how to implement this.

For the specifics of binding the columns of the DataGrid I strongly recommend Meleak's answer to that question.

Similar to that you can use attached properties (or Behaviors) and still maintain a clean ViewModel in MVVM.

I know the learning curve for WPF is quite steep and you're struggling already. I also know that the following suggestion doesn't help that and even makes that curve steeper. But your scenario is complex enough that I'd recommend to use PRISM.

Community
  • 1
  • 1
Markus Hütter
  • 7,796
  • 1
  • 36
  • 63
  • thank you for reply, attached properties will do the job I think, I'm not trying to maintain clean View or ViewModel, I'm just trying to get the job done. I'm not sure about PRISM (never seen this but I know that it's MVVM framework) because the problem lies in poorly designed WPF's controls (like DataGrid), and the solution is to use\write another table control, or use event's in one form or another (like attached properties) – Alex Burtsev Feb 27 '11 at 21:48
  • @Alex having to rely on events in the form of a Behavior is not a bad thing, as a Behavior is some sealed form of code that is in itself easily [testable](http://stackoverflow.com/questions/2297072/unit-test-an-attached-behavior-wpf). Of course if all you care about is getting the job done (and neither you nor anybody else has to maintain that code) you can spare yourself this, as it does involve a little overhead. – Markus Hütter Feb 27 '11 at 22:08
  • +1 for recommending PRISM for managing the general layout - the 'regions' management make life much easier. We had the same need for dynamically binding the columns so we created a custom control. It's a fair amount of work but you'll know a lot more about WPF by the end of if it :). If you're pushed for time you could break the MVVM pattern to add the columns in code then find a cleaner solution later. – David Masters Mar 01 '11 at 15:30
1

I wrote an article and a sample application with source code available, where I discuss and show the problems I have mentioned here and how to solve them.

http://alexburtsev.wordpress.com/2011/03/05/mvvm-pattern-in-silverlight-and-wpf/

Alex Burtsev
  • 12,418
  • 8
  • 60
  • 87
0

In order to connect your ViewModel to your View you need to assign the View's DataContext. This is normally done in the View's Constructor.

public View()
{
    DataContext = new ViewModel();
}

If you'd like to see your view model's effect at design time, you need to declare it in XAML, in the View's resources, assign a key to it, and then set the target's DataContext via a StaticResource.

<UserControl
xmlns:vm="clr-namespace:MyViewModels
>
    <UserControl.Resources>
         <vm:MyViewModel x:Key="MyVM"/>
    </UserControl.Resources>
    <MyControl DataContext={StaticResource MyVM}/>
</UserControl>

(The above is to demonstrate the design-time trick works)

Since you're dealing with a scenario that includes a container such as the TabControl I'd advocate considering the following things :

  • Hold your child ViewModels in a Property of type ObservableCollection
  • Bind the TabControls ItemsSource to that property
  • Create a new View that derives from TabItem
  • Use a template selector to automatically pick the type of the view based on the type of the view model.
  • Add IDisposable to yoour child ViewModels and create functionality to close the views.

Hope that helps a bit, if you have further questions let me know.

Maciek
  • 19,435
  • 18
  • 63
  • 87
  • Yeah it does help a bit, I haven't heard about template selectors. Though I'm not so new to not know about DataContext :-) – Alex Burtsev Feb 27 '11 at 14:14
  • Alex, consider the following example, you have a view MyView that has a control of type ItemsControl (displays a list of items). In XAML you write : . This may not have the desired effect event if your MyItemsProperty is correctly initialized and contains items. This is because the MyItemsControl is unaware about it's DataContext. In this case, in MyView's constructor you'd need to write MyItemsControl.DataContext = this. Now MyItemsControl knows where to look (continued) – Maciek Feb 27 '11 at 14:17
  • If you had multiple controls in that same view under the same scenario you'd need to repeat that n-times. The purpose of MVVM is to eliminate that with ONE SWIFT STROKE (DataContext = new ViewModel()). All controls in the view target the ViewModel as their DataContext. – Maciek Feb 27 '11 at 14:20
  • DataGrid is a bit trickier, I'd tackle it in the ChildView and have it data-bound to the ChildViewModel - a lot depends on what DataGrid are you using, is it the one from WPF Toolkit or a different one, this control can get pretty nasty. – Maciek Feb 27 '11 at 14:24
  • What about Binding DataGrid's Column's, sorted column (which I need in my UI logic). I'm using DataGrid that ships with Net 4.0. I know how to bind things that are easy bindable through ItemsSource, but ReadOnly properties, properties that are not Dependency properties, or things that don't have property at all like currently sored column of DataGrid, make me stuck. – Alex Burtsev Feb 27 '11 at 14:31