Search code examples
xamllistviewbindinguwpmaster-detail

MasterDetail ListView and editable ContentPresenter: what is wrong?


I'm based on the official Microsoft sample to create a MasterDetail ListView: MasterDetail ListView UWP sample

I have adapted it to my case, as I want that users can edit directly selected items from the ListView. But I meet a strange comportement:

  • when I add a new item to the ListView, the changes of the current item, done in the details container, are well saved
  • but when I select an existing item in the ListView, the changes of the current item, done in the details container, are not saved

Here is a screenshot of my app: screenshot

The XAML of my ListView is like this:

<!-- Master : List of Feedbacks -->
<ListView
    x:Name="MasterListViewFeedbacks"
    Grid.Row="1"
    ItemContainerTransitions="{x:Null}"
    ItemTemplate="{StaticResource MasterListViewFeedbacksItemTemplate}"
    IsItemClickEnabled="True"
    ItemsSource="{Binding CarForm.feedback_comments}"
    SelectedItem="{Binding SelectedFeedback, Mode=TwoWay}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.FooterTemplate>
        <DataTemplate>
            <CommandBar Background="White">
                <CommandBar.Content>
                    <StackPanel Orientation="Horizontal">
                        <AppBarButton Icon="Add" Label="Add Feedback"
                                  Command="{Binding AddItemFeedbacksCommand}" />
                        <AppBarButton Icon="Delete" Label="Delete Feedback"
                                  Command="{Binding RemoveItemFeedbacksCommand}" />
                    </StackPanel>
                </CommandBar.Content>
            </CommandBar>
        </DataTemplate>
    </ListView.FooterTemplate>
</ListView>

The XAML of the ListView's ItemTemplate is:

<DataTemplate x:Key="MasterListViewFeedbacksItemTemplate" x:DataType="models:Feedback_Comments">
    <StackPanel Margin="0,11,0,13"
                Orientation="Horizontal">
        <TextBlock Text="{x:Bind creator }" 
                   Style="{ThemeResource BaseTextBlockStyle}" />
        <TextBlock Text=" - " />
        <TextBlock Text="{x:Bind comment_date }"
                   Margin="12,1,0,0" />
    </StackPanel>
</DataTemplate>

The XAML of the Details container is like this:

<!-- Detail : Selected Feedback -->
<ContentPresenter
    x:Name="DetailFeedbackContentPresenter"
    Grid.Column="1"
    Grid.RowSpan="2"
    BorderThickness="1,0,0,0"
    Padding="24,0"
    BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
    Content="{x:Bind MasterListViewFeedbacks.SelectedItem, Mode=OneWay}">
    <ContentPresenter.ContentTemplate>
        <DataTemplate x:DataType="models:Feedback_Comments">
            <StackPanel Visibility="{Binding FeedbacksCnt, Converter={StaticResource CountToVisibilityConverter}}">

                <TextBox Text="{Binding creator, Mode=TwoWay}" />
                <DatePicker Date="{Binding comment_date, Converter={StaticResource DateTimeToDateTimeOffsetConverter}, Mode=TwoWay}"/>
                <TextBox TextWrapping="Wrap" AcceptsReturn="True" IsSpellCheckEnabled="True"
                         Text="{Binding comment, Mode=TwoWay}" />
            </StackPanel>
        </DataTemplate>
    </ContentPresenter.ContentTemplate>
    <ContentPresenter.ContentTransitions>
        <!-- Empty by default. See MasterListView_ItemClick -->
        <TransitionCollection />
    </ContentPresenter.ContentTransitions>
</ContentPresenter>

The "CarForm" is the main object of my ViewModel. Each CarForm contains a List of "Feedback_Comments".

So in my ViewModel, I do this when I add a new comment:

private void AddItemFeedbacks()
{
    FeedbacksCnt++;
    CarForm.feedback_comments.Add(new Feedback_Comments()
    {
        sequence = FeedbacksCnt,
        creator_id = user_id,
        _creator = username,
        comment_date = DateTime.Now
    });
    SelectedFeedback = CarForm.feedback_comments[CarForm.feedback_comments.Count - 1];
}

=> the changes done in the Feedback_Comment that was edited before the add are well preserved

I don't do anything when the user select an existing Feedback_Comment: this is managed by the XAML directly.

=> the changes done in the Feedback_Comment that was edited before to select anoter one are not preserved

=> Would you have any explanation?


Solution

  • The TwoWay binding for the Text property is updated only when the TextBox loses focus. However, when you select a different item in the list, the contents of the TextBox are no longer bound to the original item and so are not updated.

    To trigger the update each time the Text contents change, so that the changes are reflected immediately, set the UpdateSourceTrigger set to PropertyChanged:

    <TextBox Text="{Binding comment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    

    Triggering changes everywhere

    To ensure your changes are relflected everywhere including the list, you will need to do two things.

    First, your feedback_comments is of type ObservableCollection<Feedback_Comments>. This ensures that the added and removed items are added and removed from the ListView.

    Second, the Feedback_Comments class must implement the INotifyPropertyChanged interface. This interface is required to let the user interface know about changes in the data-bound object properties.

    Implementing this interface is fairly straightforward and is described for example on MSDN.

    The quick solution looks like this:

    public class Feedback_Comments : INotifyPropertyChanged
    { 
        // your code
    
        //INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;
    
        private void OnPropertyChanged( [ CallerMemberName ]string propertyName = "" )
        {
            PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }
    

    Now from each of your property setters call OnPropertyChanged(); after setting the value:

    private string _comment = "";
    public string Comment
    {
       get
       {
           return _comment;
       }
       set
       {
           _comment = value;
           OnPropertyChanged();
       }
    }
    

    Note, that the [CallerMemberName] attribute tells the compiler to replace the parameter by the name of the caller - in this case the name of the property, which is exactly what you need.

    Also note, that you can't use simple auto-properties in this case (because you need to call the OnPropertyChanged method.

    Bonus

    Finally as a small recommendation, I see you are using C++-like naming conventions, which does not fit too well into the C# world. Take a look at the recommended C# naming conventions to improve the code readability :-) .