1
votes

I am using two ListBox elements. One is the parent (BucketListBox), which decides what is supposed to be displayed in child. Child (ObjectListBox) is in multi-select mode. Once I choose particular elements in child, I shift to another parent, the child ListBox is updated with new choices.

When the user shifts back to a previously browsed parent, he should be able to see the old choices in the child that he had selected then. I am maintaining a list of the items user selects in child. I want to use this list to change background color of those elements he had previously selected.

As of now, those elements are still selected, but not shown in UI.

Here is my XAML code for the parent:

<ListBox Grid.Row="1" x:Name="BucketListBox" HorizontalContentAlignment="Stretch" SelectionChanged="BucketListBox_SelectionChanged">
<ListBox.ItemTemplate>
    <DataTemplate>
        <Grid Margin="10,10,10,10">
            <Grid.RowDefinitions>
                <RowDefinition Height="2*"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <RelativePanel Grid.Row="0">
                <TextBlock x:Name="BucketNameLabel" x:Uid="NameLabel"/>
                <TextBlock Grid.Row="0" x:Name="BucketNameTextBlock" RelativePanel.AlignBottomWith="BucketNameLabel" RelativePanel.RightOf="BucketNameLabel" Text="{Binding BucketName}" Margin="10,10,10,10"/>
            </RelativePanel>
            <RelativePanel Grid.Row="1">
                <TextBlock x:Name="BucketCreationDateLabel" FontSize="12" x:Uid="CreationDateLabel" HorizontalAlignment="Right"/>
                <TextBlock x:Name="BucketCreationDateTextBlock" FontSize="12" RelativePanel.RightOf="BucketCreationDateLabel" RelativePanel.AlignBottomWith="BucketCreationDateLabel" Text="{Binding BucketCreationDate}" Margin="10,10,10,10" HorizontalAlignment="Right" />
            </RelativePanel>
        </Grid>
    </DataTemplate>
</ListBox.ItemTemplate>

Code for child ListBox:

<ListBox Grid.Row="1" Grid.Column="1" x:Name="ObjectListBox" HorizontalContentAlignment="Stretch" SelectionMode="Multiple">

        <ListBox.ItemTemplate >
            <DataTemplate >
                <Grid Margin="10,10,10,10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="2*"/>
                        <RowDefinition Height="1*"/>
                        <RowDefinition Height="1*"/>
                    </Grid.RowDefinitions>
                    <RelativePanel Grid.Row="0">
                        <TextBlock x:Name="NameLabel" x:Uid="NameLabel" Margin="10,10,10,10" />
                        <TextBlock x:Name="ObjectNameTextBlock" Text="{Binding ObjectName}" RelativePanel.RightOf="NameLabel" RelativePanel.AlignBottomWith="NameLabel" Margin="10,10,10,10" />
                    </RelativePanel>
                    <RelativePanel Grid.Row="1">
                        <TextBlock x:Name="SizeLabel" FontSize="12" x:Uid="SizeLabel"/>
                        <TextBlock x:Name="ObjectSize" FontSize="12" Margin="10,10,10,10" Text="{Binding Path=ObjectSize}" RelativePanel.RightOf="SizeLabel" RelativePanel.AlignBottomWith="SizeLabel"/>
                    </RelativePanel>
                    <RelativePanel Grid.Row="2">
                        <TextBlock x:Name="ObjectModificationDateLabel" FontSize="12" x:Uid="ModificationDateLabel" Margin="10,10,10,10" HorizontalAlignment="Right"/>
                        <TextBlock x:Name="ObjectModificationDateTextBlock" FontSize="12" Text="{Binding ObjectLastModificationDate}" Margin="10,10,10,10" RelativePanel.RightOf="ObjectModificationDateLabel" RelativePanel.AlignBottomWith="ObjectModificationDateLabel" HorizontalAlignment="Right"/>
                    </RelativePanel>

                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

And here is the code for updating ListBoxItem: (called whenever the user clicks on a new item in parent ListBox)

private void UpdateListBoxWithSelectedItems()
    {
        List<S3ObjectInfoHolder> currentList = (List<S3ObjectInfoHolder>)ObjectListBox.ItemsSource; //Getting the total elements being displayed in the current child list box
        foreach (S3ObjectInfoHolder entry in listOfObjectsToTransfer) //listOfObjectsToTransfer is the list of all the entries from all the parents selected so far. So the intersection of currentList with this list gives the ones that are to be colored on UI so as the user knows the items are still selected
        {
            for (int i = 0; i < currentList.Count(); i++)
            {
                if (currentList[i].ObjectName.Equals(entry.ObjectName))
                {
                    ObjectListBox.SelectedItems.Add(entry);
                    Debug.WriteLine("Selected Item:" + entry.ObjectName);
                }
            }
        }
        List<object> selectedItem = ObjectListBox.SelectedItems.ToList();
        foreach (object item in selectedItem)
        {
            ListBoxItem selectedListBoxItem = ObjectListBox.ItemContainerGenerator.ContainerFromItem((S3ObjectInfoHolder)item) as ListBoxItem;
            if(selectedListBoxItem!=null) selectedListBoxItem.Background = new SolidColorBrush(Colors.Turquoise);
        }
    }

selectedListBoxItem!=null fails, and the line is skipped. And lastly, here is the code for the binding class:

class S3ObjectInfoHolder
{
    private string key;
    private string bucketName;
    private long size;
    private string isObjectAFolder;
    private string objectLastModificationDate;

    public S3ObjectInfoHolder(string key, string bucketName)
    {
        this.key = key;
        this.bucketName = bucketName;
        isObjectAFolder = isObjectAFolderOrAFile(key);
    }

    private string isObjectAFolderOrAFile(string key)
    {
        if (key[key.Length - 1] == '/')
        {
            return "is a Folder";
        }
        else
        {
            return "is a File";
        }
    }

    public string ObjectName
    {
        get
        {
            return key;
        }

        set
        {
            key = value;
        }
    }

    public long ObjectSize
    {
        get
        {
            return size;
        }

        set
        {
            size = value;
        }
    }

    public string ObjectIsFolder
    {
        get
        {
            return isObjectAFolder;
        }

        set
        {
            isObjectAFolder = value;
        }
    }

    public string BucketName
    {
        get
        {
            return bucketName;
        }

        set
        {
            bucketName = value;
        }
    }

    public string ObjectLastModificationDate
    {
        get
        {
            return objectLastModificationDate;
        }

        set
        {
            objectLastModificationDate = value;
        }
    }

}
1
What does "shift" mean to you? Change focus?15ee8f99-57ff-4f92-890c-b56153
He clicks on a different item in parent ListBox (which basically shows Amazon S3 Buckets). When he clicks on this new bucket, the contents of that bucket are shown. Now I need to color UI dynamically so that when he comes back to this bucket where he had chosen some items in child, those again get colored.Ayush Soni
It looks like you have code to do that already. Is something not working right?15ee8f99-57ff-4f92-890c-b56153
selectedListBoxItem!=null fails, and the line is skipped. I don't know why. Am I doing something wrong there? (See the last five lines of UpdateListBoxWithSelectedItems() function)Ayush Soni

1 Answers

1
votes
  1. S3ObjectInfoHolder needs to implement INotifyPropertyChanged.

    public class NotificationBase : INotifyPropertyChanged {
        protected void RaisePropertyChanged([CallerMemberName] string property = null)
        {
            var handler = PropertyChanged;
            if (handler != null) 
            { 
                handler(this, new PropertyChangedEventArgs(property)); 
            }
        }
    }
    
    public class S3ObjectInfoHolder : NotificationBase
    {
        //  ...
    
  2. Add this property to S3ObjectInfoHolder

    public class S3ObjectInfoHolder : NotificationBase
    {
        //  ...
    
        private bool _isSelected = false;
        public bool IsSelected {
           get { return _isSelected; }
           set {
                if (_isSelected != value) {
                    _isSelected == value;
                    RaisePropertyChanged(nameof(IsSelected));
                }
            }
        }
    
        //  ...
    
  3. Write a value converter that chooses between two brushes based on a boolean value. This can go in your codebehind, or in a separate file.

    public class BrushChooserConverter : IValueConverter
    {
        public Brush TrueBrush { get; set; }
        public Brush FalseBrush { get; set; }
    
        public object Convert(object value, Type targetType,
            object parameter, string language)
        {
            return System.Convert.ToBoolean(value)
                        ? TrueBrush
                        : FalseBrush;
        }
    
        // No need to implement converting back on a one-way binding 
        public object ConvertBack(object value, Type targetType,
            object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
    
  4. Create an instance of the value converter in the ListBox's Resources, and use the converter to set the Grid background in your data template:

     <ListBox.Resources>
         <local:BrushChooserConverter
             x:Key="SelectedItemBrushChooser"
             TrueBrush="Turquoise"
             FalseBrush="Transparent"
             />
     </ListBox.Resources>
     <ListBox.ItemTemplate>
         <DataTemplate>
             <Grid 
                 Background="{Binding IsSelected, Converter={StaticResource SelectedItemBrushChooser}}"
                 Margin="10">
                 <Grid.RowDefinitions>
                     <!-- ... -->
    
  5. Set properties on the data items and let the XAML do the work. Don't mess with controls in code behind, it's always a mess because XAML controls aren't meant to be used that way. In a XAML ListBox, any ListBoxItem that isn't currently visible will be reused for another data item. This is called "virtualization"; it cuts down on the number of controls that are created.

    So when the user scrolls this thing, when an item with a colored background is scrolled out of view, that actual ListBoxItem control will be reused for the next item that is scrolled into view. It will still have the same background color you gave it. Will that be correct? Often not, and there is no event you can handle to step in and correct it. No event at all. That's because you should be using databinding anyway instead of messing with the controls directly.

    You bought yourself a hideous can of worms trying to do this in codebehind. It's worse than you know. But the databinding approach I'm showing you is much more error-proof, so it's all good.

    You should have a main viewmodel instead of doing this in codebehind; I urge you to research that.

    All a viewmodel really is, is a class that inherits from the NotificationBase I gave you, or from some other base class just like it. A viewmodel's properties raise notifications when they change, and it uses ObservableCollection<T> instead of List<T> for any collection that might have items added or removed while it's being used as an ItemsSource.

    S3ObjectInfoHolder, with my changes, is starting to become a viewmodel. It's not the main viewmodel. It's a child viewmodel. You should add the RaisePropertyChanged call to every property it has. When you do that, and you use a Binding with that property in XAML, the Binding will be notified when the property's value changes, and the UI will update automatically.

    But for now, I'll just show you how to modify what you've got.

    I don't understand your code. I don't know why you're comparing names. Maybe currentList[i] and entry are the same actual object; I can't guess.

    private void UpdateListBoxWithSelectedItems()
    {
        List<S3ObjectInfoHolder> currentList = (List<S3ObjectInfoHolder>)ObjectListBox.ItemsSource; 
        foreach (S3ObjectInfoHolder entry in listOfObjectsToTransfer) {
            for (int i = 0; i < currentList.Count(); i++)
            {
                if (currentList[i].ObjectName.Equals(entry.ObjectName))
                {
                    ObjectListBox.SelectedItems.Add(entry);
                    entry.IsSelected = true;
                    Debug.WriteLine("Selected Item:" + entry.ObjectName);
                }
                else
                {
                    entry.IsSelected = false;
                }
            }
        }
    

    And get rid of the second loop with the container stuff.