Binding HierarchicalDataTemplate to ExpressionObserver.Execute

Apr 15, 2010 at 3:31 PM

Thomas, How've you been.

I had asked you a question in the previous thread about Binding a Treeview to an ExpressionObserver (http://obtics.codeplex.com/Thread/View.aspx?ThreadId=205004). I have explained the scenario in detail in teh previous post, where I have an Observable collection of raw values and am trying to create a hierarchicalview grouped by Dest and SubDest. I am successfully able to bind the OrdersgroupedBySubDest (as per previous post) to the treeview. However I have noticed that as the ObservableCollection triggers the NotifyChanged event, the treeview automatically collapses and loses its ViewState. By ViewState, i mean that if certain nodes are open, they do reflect the change but also collapse. I have attached the snippet below. The tree binds to OrdersGroupedBySubDest. Below are the ExpressionObserver definition and the TreeView definitions.

There are no issues in binding. However when the underlying observablecollection (childOrdersList) gets updated, the treeview collapses to its original form. I was trying to keep it to maintain its current state e.g. if nodes 2 and 3 are open along with 2(a) and 3(b), then the tree should nt collapse on a notificationChange.

Again any help in this regards will be greatly appreciated.

1
2
  a
3
  a
  b
     bi
     bii
4
5
public IEnumerable<OutboundSummary> OrdersGroupedBySubDest
        {
            get
            {
                return
                    OE.Async(
                            ExpressionObserver.Execute(
                                () =>
                                    (
                                        childOrdersList.GroupBy(x => new { x.SubDest })
                                            .Select(x => new OutboundSummary
                                            {
                                                SubDest = x.First().SubDest,
                                                TotalOrders = x.Count(c => c.isActive),
                                                TotalShares = x.Sum(c => c.ExecShares),
                                                OutboundGroups = (from d in x
                                                                  group d by new { d.Dest } into pg
                                                                  select new OutboundSummary()
                                                                  {
                                                                      Dest = pg.Key.Dest,
                                                                      TotalOrders = pg.Count(c => c.isActive),
                                                                      TotalShares = pg.Sum(c => c.ExecShares),
                                                                      OutboundGroups = (
                                                                            from d in pg
                                                                            group d by new { d.Src } into pg2
                                                                            select new OutboundSummary()
                                                                            {
                                                                                System = pg2.First().Src,
                                                                                TotalOrders = pg.Count(c=>c.isActive),
                                                                                TotalShares = pg.Sum(c => c.ExecShares),
                                                                            }),
                                                                  }),

                                            })//.OrderByDescending(c => c.SubDest)
                                    )
                            ).Cascade()
                        );
            }
        }
		<Grid Grid.IsSharedSizeScope="True">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <Grid.Resources>
                                <DataTemplate x:Key="OrderDetailTemplate">
                                    <TextBlock>
                                        <TextBlock Text="{Binding Path=System}" />
                                        <Run>(</Run>
                                        <TextBlock Text="{Binding Path=TotalShares}" />
                                        <Run>,</Run>
                                        <TextBlock Text="{Binding Path=TotalOrders}" />
                                        <Run>)</Run>
                                  </TextBlock>
                                </DataTemplate>
                                <!-- ORDER TEMPLATE -->
                                <HierarchicalDataTemplate 
                                      x:Key="OrderTemplate"
                                      ItemsSource="{Binding OutboundGroups}"
                                      ItemTemplate="{StaticResource OrderDetailTemplate}">
                                    <TextBlock Text="{Binding Path=SubDest}">
                                        <Run>(</Run>
                                        <TextBlock Text="{Binding Path=TotalShares}" />
                                        <Run>,</Run>
                                        <TextBlock Text="{Binding Path=TotalOrders}" />
                                        <Run>)</Run>
                                     </TextBlock>
                                </HierarchicalDataTemplate>

                                 <!-- CUSTOMER TEMPLATE -->
                                 <HierarchicalDataTemplate 
                                      x:Key="CustomerTemplate"
                                      ItemsSource="{Binding OutboundGroups}"
                                      ItemTemplate="{StaticResource OrderTemplate}">
                                    <TextBlock>
                                        <TextBlock Text="{Binding Path=Dest}" />
                                        <Run>(</Run>
                                        <TextBlock Text="{Binding Path=TotalShares}" />
                                        <Run>,</Run>
                                        <TextBlock Text="{Binding Path=TotalOrders}" />
                                        <Run>)</Run>
                                     </TextBlock>
                                </HierarchicalDataTemplate>
                            </Grid.Resources>
                            <TreeView
                                Name="_treeView"
                                ItemsSource="{Binding Path=.}"
                                ItemTemplate="{StaticResource CustomerTemplate}"
                             />
                        </Grid>
BTW this is one of the best reactive linq libraries out there. I hope you continue to maintain this as the versions of silverlight and WPF update.
Apr 15, 2010 at 4:50 PM

On a similar note. The selectedIndex of the treeview also changes. The same happens when the Result from ExpressionObserver is binded to a datagrid.

Coordinator
Apr 20, 2010 at 6:44 PM

Hi,

Could it be that your OutboundSummary class does NOT have an equals override? In that case the treeview may not be able to logically match two OutboundSummaries. (An old and a new one that was created after an update. The treeview would recognize it as a completely new and different node) 

Regards,

Thomas

Apr 20, 2010 at 7:36 PM

Would it be a new node even if its just a value update? Can you give me an example of how I should differentiate the instances in th Equals method? I am grouping it by 3 different attributes Dest | SubDest | System.

Thanks

Vinay

Apr 20, 2010 at 8:56 PM

I attempted to add the following Equals and GetHashCode methods. I basically want to display any change where the values of Totalorders and Execshares change. this will happen if any of the attributes within the 3 groups (Dest/SubDest/System) change. An idea or approach will be appreciated.

         public override bool Equals(object obj)
        {
            bool ret = true;
            OutboundSummary o = obj as OutboundSummary;

            if (!string.IsNullOrEmpty(o.SubDest) && !string.IsNullOrEmpty(this.SubDest))
            {
                if (this.SubDest.Equals(o.SubDest) && this.ExecShares == o.ExecShares && this.TotalShares == o.ExecShares)
                    ret = true;
                else
                    ret = false;
            }
            else if (!string.IsNullOrEmpty(o.Dest) && !string.IsNullOrEmpty(this.Dest))
            {
                if (this.Dest.Equals(o.Dest) && this.ExecShares == o.ExecShares && this.TotalShares == o.ExecShares)
                    ret = true;
                else
                    ret = false;
            }
            else if (!string.IsNullOrEmpty(o.System) && !string.IsNullOrEmpty(this.System))
            {
                if (this.System.Equals(o.System) && this.ExecShares == o.ExecShares && this.TotalShares == o.ExecShares)
                    ret = true;
                else
                    ret = false;
            }
            else
                ret = false;

            return ret;
            //return base.Equals(obj);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

 

Coordinator
Apr 22, 2010 at 9:47 PM

Would an equals like this work? :

public override bool Equals(object other)
{
    var o = other as OutboundSummary;

    return
        o != null
        && object.Equals(SubDest, o.SubDest)
        && object.Equals(Dest, o.Dest)
        && object.Equals(System, o.System)
    ;
}

public override int GetHashCode()
{
    return
        (SubDest == null ? 0 : SubDest.GetHashCode())
        + (Dest == null ? 0 : Dest.GetHashCode())
        + (System == null ? 0 : System.GetHashCode())
    ;
}

I Guess the Orders and Shares would not be part of the identity of your node. (since they may vary for what is logically the same node)

Regs,

Thomas

Apr 23, 2010 at 3:53 PM

Thomas, thanks for the response. I will give this a try.

Apr 23, 2010 at 6:58 PM

I did try the approach you mentioned above. The issue is even though the tree does not collapse. I do not see any updates. The way the code is supposed to work is the upper level tree is suupposed to reflect the changes when a value within the lowest group changes. Since the aggregation trickles upward from the System to Dest to SubDest, any particular value update on the raw object should (within the observablecollection) should show the appropriate value update on the tree items too.

Also I notice that the equals function does not actually get hit until I open a particular treeitem. Is that how it should work?

Coordinator
Apr 25, 2010 at 7:10 PM

I see how my last reply didn't help at all. I'm afraid it can not be done in a single statement. The TotalOrders and TotalShares properties will need to be observable.

Possibly this code will help? :

    public abstract class OutboundSummary<TKey>
    {
        public IGrouping<TKey, X> _Src;

        public override bool Equals(object obj)
        {
            var other = obj as OutboundSummary<TKey>;
            return
                other != null
                && this.GetType() == other.GetType()
                && EqualityComparer<TKey>.Default.Equals(_Src.Key, other._Src.Key)
            ;
        }

        public override int GetHashCode()
        { return this.GetType().GetHashCode() + EqualityComparer<TKey>.Default.GetHashCode(_Src.Key); }

        static Func<IEnumerable<X>, IValueProvider<int>> _OrdersF =
            ExpressionObserver.Compile(
                (IEnumerable<X> grp) => grp.Count(c => c.IsActive)
            )
        ;

        public IValueProvider<int> TotalOrders { get { return _OrdersF(_Src); } }

        static Func<IEnumerable<X>, IValueProvider<decimal>> _SharesF =
            ExpressionObserver.Compile(
                (IEnumerable<X> grp) => grp.Sum(c => c.ExecShares)
            )
        ;

        public IValueProvider<decimal> TotalShares { get { return _SharesF(_Src); } }
    }

    public class SrcSummary : OutboundSummary<string>
    { public string System { get { return _Src.Key; } } }

    public class DestSummary : OutboundSummary<long>
    {
        public long Dest { get { return _Src.Key; } }

        static Func<IEnumerable<X>, IValueProvider<IEnumerable<SrcSummary>>> _OutboundGroupsF =
            ExpressionObserver.Compile(
                (IEnumerable<X> grp) =>
                    from d in grp
                    group d by d.Src into pg
                    select new SrcSummary { _Src = pg }
            )
        ;

        public IEnumerable<SrcSummary> OutboundGroups { get { return _OutboundGroupsF(_Src).Cascade(); } }
    }

    public class SubDestSummary : OutboundSummary<int>
    {
        public int SubDest { get { return _Src.Key; } }

        static Func<IEnumerable<X>, IValueProvider<IEnumerable<DestSummary>>> _OutboundGroupsF = ExpressionObserver.Compile(

            (IEnumerable<X> grp) => from d in grp
                    group d by d.Dest into pg
                    select new DestSummary { _Src = pg }
            )
        ;

        public IEnumerable<DestSummary> OutboundGroups { get { return _OutboundGroupsF(_Src).Cascade(); } }
    }

    public IEnumerable<SubDestSummary> OrdersGroupedBySubDest
    {
        get
        {
            OE.Async(
                ExpressionObserver.Execute(
                    () =>
                        from d in childOrdersList
                        group d by d.SubDest into pg
                        select new SubDestSummary { _Src = pg }
                )
                .Cascade()
            )
        }
    }

The types of Dest, SubDest etc. are probably not what they should be but you can fix that. In XAML you wouldn't bind to the TotalShares and TotalOrders properties directly. These properties now return IValueProvider objects and you need to bind to their Value properties.

Hope this does help,

Regs,

Thomas. 

 

 

Coordinator
Apr 25, 2010 at 7:18 PM

I Forgot:

If your source is updated on different threads than the UI thread then you need to aply an Async call to all the observable properties (TotalShared, TotalOrders and OutboundGroups)

Regards,

Thomas