Changing the bindingContext of a converter parameter within a Listview

pavpav Member ✭✭

What i'm trying to do is Change the background color of a CollectionView (preview xf 4.0) element based on a selected value. element can be selected or running.

<CollectionView Grid.ColumnSpan="3" Grid.RowSpan="2" ItemsSource ="{Binding Projects}" SelectionMode="Single" 
            SelectedItem="{Binding SelectedProject, Mode=TwoWay}"
            SelectionChangedCommand="{Binding SelectActiveProjectCommand}"   >
    <CollectionView.ItemsLayout>
        <GridItemsLayout Orientation="Vertical" Span="3" />
    </CollectionView.ItemsLayout>

    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Frame Padding="10" BackgroundColor="{StaticResource Background}" HasShadow="False" CornerRadius="0" >
                <Grid  Padding="5" BackgroundColor="{Binding Id, Converter={StaticResource BackgroundConverter}, ConverterParameter= {Binding Source={x:Reference RootPage}, Path=BindingContext}}" >
                   <!-- removded for brevity -->
                </Grid>
            </Frame>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

When you take a look at the Grid background color, you see that i bind to the models ID property, but that the convertereParamer is bound to the RootPage and not the page's bindingcontext, which is my first code smell, for some reason i have to do it this way then extract the property i want within the converter and can't just bind to it. I have a feeling that i'm doing something wrong.

public class HighlightProjectConverter : IValueConverter
{
    static Color contrastColor = (Color)Application.Current.Resources["ContrastColor"]   ;
    static Color backgroundContrastColor = (Color)Application.Current.Resources["BackgroundContrast"];
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var ProjectID = Guid.Parse(value.ToString());

        var a = ((Binding)parameter).Source as MainPage;
        var bc = a.BindingContext as MainPageViewModel;

        return bc.ActiveTask?.ProjectId == ProjectID ? contrastColor : backgroundContrastColor;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

So, i'd really like to know if i'm doing something wrong here or if there's a better way that i'm missing, it seems silly to me that i Can't just bind to the BindingContext of the page or even teh ActiveTask property.

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai

    ConverterParameter is not a Dependency Property, therefore it can't be used for binding.
    You could try my manner to change the Grid's background when it is selected.
    Define a PreviousModel property in your view model and make your SelectedProject like:

    Model selectedProject;
    public Model SelectedProject
    {
        set
        {
            if (selectedProject != value)
            {
    
                if (PreviousModel != null)
                {
                    PreviousModel.BgColor = Color.White;
                }
                selectedProject = value;
                selectedProject.BgColor = Color.Red;
                PreviousModel = value;
                onPropertyChanged();
            }
    
        }
        get
        {
            return selectedProject;
        }
    }
    
    public Model PreviousModel { set; get; }
    

    The code above changes the BgColor property which is also needed to be added in your collection view model and the Grid's background color has been bound to that property:

    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Frame Padding="10" BackgroundColor="{StaticResource Background}" HasShadow="False" CornerRadius="0" >
                <Grid  Padding="5" BackgroundColor="{Binding BgColor}" >
                    <!-- removded for brevity -->
                </Grid>
            </Frame>
        </DataTemplate>
    </CollectionView.ItemTemplate>
    

    Color.White is the Grid's original color and Color.Red is the selected color you want it to be. Change them as you want.

  • pavpav Member ✭✭
    edited April 28

    I'm really not thrilled with putting styling into my viewmodel, I'm more ok with using the converter method as clunky as it feels. For now i'm going to bind the ConverterParameter to the reference of the contentPage like so

    <Grid  Padding="5" BackgroundColor="{Binding Id, Converter={StaticResource BackgroundConverter}, ConverterParameter= {x:Reference RootPage}}" >
        <Grid.RowDefinitions>
            <RowDefinition Height="35"/>
            <RowDefinition Height="35"/>
            <RowDefinition Height="35"/>
        </Grid.RowDefinitions>
        <Label Grid.Row="0" Text="{Binding ClientName}" FontSize="Large"/>
        <Label Grid.Row="1" Text="{Binding ProjectName}" FontSize="Large"/>
        <Label Grid.Row="2" Text="0.0%" FontSize="Large" HorizontalTextAlignment="End"  VerticalOptions="End" />
    </Grid>
    

    then i'm going to extract the BindingContext from the ContentPage in the ValueConverter and cast it appropriately.

    public class HighlightProjectConverter : IValueConverter
    {
        public Color ContrastColor { get; set; }
        public Color NormalColor { get; set; }
    
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var ProjectID = Guid.Parse(value.ToString());
            var bc = ((ContentPage)parameter).BindingContext as MainPageViewModel;
            return bc.ActiveTask?.ProjectId == ProjectID ? ContrastColor : NormalColor;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    this really limits the reuse of the Converter however i'm more OK with that than putting styling into my model or viewmodel.

    For completeness here's my Converter declared inside my Content pages resources.

        <converters:HighlightProjectConverter x:Key="BackgroundConverter" ContrastColor="{StaticResource ContrastColor}" NormalColor="{StaticResource BackgroundContrast}" />
    

    and of course the content page has a name property that aligns with the reference in the converter parameter.

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:core="clr-namespace:pav.timeKeeper.mobile.Core"
                 xmlns:eff="clr-namespace:pav.timeKeeper.mobile.Effects"
                 xmlns:con="clr-namespace:pav.timeKeeper.mobile.Converters"
                 xmlns:custom="clr-namespace:pav.timeKeeper.mobile.Controls"
                 Visual="Material"
                 core:Bootstraper.AutoWireViewModel="True"
                 x:Name="RootPage"
                 x:Class="pav.timeKeeper.mobile.Views.MainPage">
    

    and my start active project Command, what this let's me do is close the current active task, and set a new one as the primary, notice that for both the selected, and the old projects i notify that the Id property has been updated meaning that only the Previously selected and currently selected projects will have their background colors updated.

     public ICommand StartActiveProjectCommand {
                get => startActiveProjectCommand = startActiveProjectCommand ?? 
                    new Command(
                        execute: async ()=> {
                            Guid? OldTaskId = ActiveTask?.ProjectId;
    
                            if(ActiveTask != null)
                            {
                                ActiveTask.End = DateTime.Now;
                                await  repo.UpdateActionableTaskAsync(ActiveTask);
                            }
    
                            if (Projects.FirstOrDefault(p => p.Id == SelectedProject.Id) is Project selectedProject)
                            {
                                selectedProject.NotifyPropertyChanged(nameof(IProject.Id));
                                ActiveTask = new ActionableTask(SelectedProject.Id, SelectedProject.Tasks[SelectedTaskIndex].Id);
                            }
    
                            if (OldTaskId.HasValue)
                            {
                                var oldProject = Projects.FirstOrDefault(p => p.Id == OldTaskId.Value) as Project;
                                oldProject.NotifyPropertyChanged(nameof(IProject.Id));
                            }
    
                            await repo.CreateActionableTaskAsync(ActiveTask);
                        }, 
                        canExecute: ()=> SelectedTaskIndex > -1);
            }
    

    @LandLu Good point that since the ConverterParameter is not a DP and that it can't be bound to, seems a bit surprising and i'm very curious as to why it was designed that way. Anyway I'm gonna leave this question open in the event that someone has a better alternative.

    Here are some pictures to help illustrate what all this nonsense is about.

    basically i need a way to visually notify the user which Project is currently being executed while letting the user browse other projects, some added context this app is just to help users better track how much effort they're putting towards which task of which project, I find that by the end of the week I have a hard time filling in my timesheet especially since i'm jumping between projects a lot. However more so this is pet project to gain a better understanding of Xamarin forms.

    if someone has a better way to pull this off, i'm all ears; if not hopefully this rant can help someone else out there.

Sign In or Register to comment.