How to pass Data

Topics: UI Architecture
Jun 1, 2012 at 7:58 AM

Hello,

I've to create a kind of dashboard that shares the data between a chart and a gridview... the main dashboard has a ObservableCollection<MyType> DataItems {get,set;} I wish to pass this data to the the two viewmodel (chart and gridview) ... for now I've used an old approach of defining the usercontrol in the xaml and pass the data as DataContext = {Binding DataContext} ... I think it's not the best way... I need this since I've got a timebar for limiting the range (and I don't want to access twice for the same data )

What's the best way I can do this?

Thanks

Jun 1, 2012 at 8:47 AM
Edited Jun 1, 2012 at 9:21 AM

I suppose you could extract an interface out of the dashboard, something like

public interface IDashboard
{
      BindableCollection<MyItem> DataItems { get; }
}

then, export the dashboard implementation using such interface

 

[Export(typeof(IDashboard))]
public class Dashboard : IDashboard
{
         //Implementation omitted
}

 

finally, every view model that require access to the DataItems collection could declare the IDashboard itself as an explicit dependency

 

[Export(typeof(Chart))]
public class Chart
{
        public IDashbaord Dashboard { get; private set; }
        
        [ImportingConstructor]
        public Chart(IDashboard dashboard)
        {
              Dashboard = dashboard;
        }
}

 Of course, you could design the interfaces and the classes the way you like, so you could even expose just the DataItems collection, instead of the entire dashboard

[Export(typeof(Chart))]
public class Chart
{
       public BindableCollection<MyItems> DataItems { get; private set; }
        
        [ImportingConstructor]
        public Chart(IDashboard dashboard)
        {
              DataItems = dashboard.DataItems;
        }
}

It is up to you to decide what is the best way to declare the dependency.

Another option is to export the collection directly, and make dashboard, chart and gridview dependendent on it... anyway, when you need to share some data among viewmodels, it is a good idea to define a dependency, using interfaces or concrete types as needed. At least, this is my preferred approach.

Jun 1, 2012 at 8:59 AM

Hello BladeWise,

meanwhile I was waiting for a reply I've tried doing so :

 

  <Grid x:Name="grid5" Grid.Row="0" Margin="10,0,0,0" Grid.ColumnSpan="2" >

                <myControls:TimelineUserControlView x:Name="TimeControl" cal:Bind.Model="{Binding TimeControl}"></myControls:TimelineUserControlView>

            </Grid>

            <!--GridIndicatori1UserControlView-->
            <Grid x:Name="grid6" Grid.Row="1"  Margin="10,0,0,0" Grid.ColumnSpan="2" >

                <myControls:GridIndicatori1UserControlView cal:Bind.Model="{Binding ModelloGrigliaIndicatori}" ></myControls:GridIndicatori1UserControlView>
            </Grid>

 

    yield return (operation = repository.GetIndicatoriFromDashboard(IdDashBoard)).ContinueOnError();

            if (operation.CompletedSuccessfully)
            {
                ListaIndicatori= new ObservableCollection<INDICATORI>(operation.Result);

                ModelloGrigliaIndicatori.DataItems = ListaIndicatori;
                TimeControl.DataItems = ListaIndicatori;
            }

 

Since I'll have different dashboard composed of different Gridview / Chart (based on the type of dashboard I've to show) (you can consider a M * N possible combination of dashboard/chart/RadGrid) what's the best approach? If I use dependency injection with IDashboard I don't know wich kind of dashboard will be retrieved (I can think about a factory that upon the dashboard id returns me my wished dashboard), how can I have it inside the chart or radgrid implementation?

 

Thanks

Jun 1, 2012 at 10:11 AM
Edited Jun 8, 2012 at 4:45 PM

You can extend the IDashbaord with an enum that define the type of dashboard... it could be even decorated with the flags attribute, so that you can define what combination of grid/chart or whathever type of view you want to display:

 

[Flags]
public enum ContentType
{
       Grid,
       Chart,
       RadGrid
}

 

this way you can re-use the same Dashboard view model to define different kind of representation.

Another option, could be to define a list of Dashboard content view-models (and I would prefer this approach):

 

public interface IDashboard
{
      BindableCollection<IDashboardContent> Contents { get; }
}
 

 

Then you could build the content of the dashboard dynamically, depending either on the enum value or the collection of contents.

Using a collection of contents would allow you to define the views for such contents indipendently, you could have three different view-models and views associated to the chart, datagrid or radgrid.

This approach is probably the most flexible, and you would be able to switch at runtime the representation of the dahsboard, if needed.

Consider creating an IDashboard view like this:

 

<UserControl ...>
    <ItemsControl x:Name="Contents"/>
</UserControl>

 

and every IDashboardContent view as something like

 

<UserControl ...>
    <Chart .../>  <!-- Customized depending on your needs -->
</Usercontrol>

 

as you can see, the container for contents is completely unaware of what is inside it, and everything is completely driven by the actual view-model. This means that the concept of dashboard is completely decoupled from the concept of its content.

And one more thing, if all contents share the same conceptual model (e.g. all of them have just the collection of data items, and all required properties are shared among different representations, like the time boundaries...), you could just define one IDashboardContent implementation, and use an enum to define the specific view that could be bound to the context of the view, so that same view model can be associated to different views.

I hope I was clear enugh... :)

Jun 1, 2012 at 4:44 PM

Hello BladeWise....

really intresting post....can you please explain me what you mean "Consider creating an IDashboard view like this:" you mean the container?

Have you got a sample application?

Thanks

Jun 1, 2012 at 8:01 PM

Yes, I mean that the dashboard view could be just a container, or if you prefer, a way to arrange different contents.

Honestly, I have no sample application, but it's just a matter of defyining proper view models and matching views, and let CM resolve 'join' them.

Jun 6, 2012 at 3:35 PM

Excuse me BladeWise if I still disturb you..... I've created a view called DashboardManager that

instead of

 

<UserControl ...> <ItemsControl x:Name="Contents"/> </UserControl>


has

   <Grid x:Name="Content" Grid.Row="1" Margin="0,10,0,0">
                <ContentControl x:Name="ActiveItem" />
            </Grid>

Since my IDashBoardContent viewmodel (called DashboardManagerViewModel is
DashboardManagerViewModel : Conductor<Screen>.Collection.OneActive, IHandle<DashBoardMessage>, INotifyPropertyChanged
)

Now I'm stucked..... I've my Dashboard1 defined via viewmodel/view ....what I should do next?

Jun 6, 2012 at 4:17 PM

If you defined the view for the ActiveItem, and the ActiveItem is not null, I suppose you should be able to see the proper content into the dashboard.

Jun 8, 2012 at 3:18 PM

Excuse me BladeWise.. there's something that's not running correctly I think..... I've defined in a Dashboard view model a usercontrol

 

     
        public TimelineUserControlViewModel TimeControl { get; set; }

 

In the DashboardView instead I've done

   <!--TimelineUserControlView-->
            <Grid x:Name="grid5" Grid.Row="0" Grid.ColumnSpan="2" >
                <ItemsControl x:Name="TimeControl"></ItemsControl>
                <!--<myControls:TimelineUserControlView x:Name="TimeControl" cal:Bind.Model="{Binding TimeControl}" ></myControls:TimelineUserControlView>-->
            </Grid>

if I use myControls it works... if I use ItemsControl not...why?? wouldn't Convention resolves the type and load it?

 

Thanks

Jun 8, 2012 at 3:34 PM
Edited Jun 8, 2012 at 3:35 PM

The default convention for ItemsControl expects to bind to a collection of objects, in other words, the type of the property named TimeControl, should implement IEnumerable.

If you need a single object ot be displayed, use a ContentControl instead of an ItemsControl.

Jun 8, 2012 at 3:44 PM

Thansk a lot!

I've still something to sharp about my viewmodel/view (for the dashboard) for example you told me to use

<ItemsControl x:Name="Contents"/>

How do I tell which view comes first? I'm still elaborating on this...


Thanks
Jun 8, 2012 at 3:56 PM

If you bind the ItemsControl to a collection, CM will automatically find the view for each view-model in the collection. Note that only the one context will be used to locate the views, so you cannot place the same view-model and hook-up multiple views for the same type.

If you want a single content in your dashboard, then the view model should have a Content property like

 

private IDashboardContent _content;

public IDashboard Content
{
     get { return _content; }
     set
     {
           if(_content != value)
           {
                  _content = value;
                RaisePropertyChanged(() => Content);
           }
     }
}

Then, the view could be something like

<Grid x:Name="grid5" Grid.Row="0" Grid.ColumnSpan="2" >
          <ContentControl x:Name="Content"/>
</Grid>

You need an ItemsControl only if you want to display at the same time, multiple contents.

If you want to allow for a dynamic change of the current dashboard content, you can use the single-content approach, and have some code to set the Content property (of the view-model) to a proper object.

Jun 8, 2012 at 3:58 PM

Excuse me again BladeWise.... since my DashboardViewModel contains a series of data that should be passed between the usercontrols (DateTimes, int, and some complex object)

what's the best approach for passing those to the inner viewmodels? should I use something like

        protected override void OnViewLoaded(object view)
        {
            base.OnViewLoaded(view);

            if(this.TimeControl != null)
                this.TimeControl.DashBoard = this;
            Caliburn.Micro.Coroutine.BeginExecute(LoadDataForDashBoard(IdDashBoard));
        }

I don't like this a lot... since my TimeControl has a reference to Dashboard...but I don't see another way... consider I can't user [Import] inside the TimeControl/GridControl since I don't know what istance will be returned (since I've more than one dashboard type.. and maybe just opened before)

 

Thanks

 

Jun 8, 2012 at 4:55 PM

If the IDashboard is not unique, the best approach, in my opinion, would be to use a factory method

public delegate IDashboardContent CreateDashboardContent(Type contentType, IDashboard dashboard);

[Export(typeof(CreateDashboardContent))]
public static IDashboardContent CreateContent(Type contentType, IDashboard dashboard)
{
        return (IDashboardContent)Activator.CreateInstance(contentType, dashboard);   //This code supposes that exists a constructor with a single parameter of type IDashboard
}

Note that this implementation uses the actual content type to decide which content should be created, but it is up to you to design the factory method to suit your needs.

I understand that you don't like the dependency over the IDashboard, but to get rid of that, you had to design a sort of State class, used to store all relevant shared information... otherwise, you are stuck with passing the IDashboard object to its contents.