I am having a strange problem in my UWP application.
I have a ListView bound to an ObservableCollection<SomeInterface>. I am changing one property of all the items in the ObservableCollection. The problem is that because of the ObservableCollection does not take a class as the type argument, rather it takes an interface, I cannot figure out how to implement INotifyCollectionChanged on the interface.
So I went ahead and changed the property of all items in the collection ( without implementing INotifyPropertyChanged ). This is giving weird behavior. When I change the property ( I do this as soon as I navigate to the page containing the ListView ) It only affects only those ListView items that are not in view currently ie, if there are 100 items in the ListView and I can see the first 10 without scrolling down then those 10 items are unchanged but when I scroll down, I see that other (other than the first 10) ListViewItems are reflecting the changes that were made. And to add to this, when I scroll up again ( to the first 10 items ), I see that now they are also changed.
to summarize, only the items that are not currently in the view get updated.
here's my code to update the ObservableCollection:
class SomePage : Page
{
private ObservabeCollection<SomeInterface> SomeObservableCollection { get; set; } = new ObservabeCollection<SomeInterface>();
...
private async Task ModifyObservableCollection()
{
var response = await MakeApiCall();
var facets = response.ListOfItems;
foreach (var item in SomeObservableCollection.ToList())
{
var fromApi = facets.author.FirstOrDefault(i => i.key == item.key);
if (fromApi == null) continue;
var itemInList = SomeList.FirstOrDefault(i => i.key == fromApi.key);
itemInList.read = fromApi.read;
itemInList.num = fromApi.num;
//here!!
itemInList = SomeObservableCollection.FirstOrDefault(i => i.key == fromApi.key);
itemInList.read = fromApi.read;
itemInList.num = fromApi.num;
}
}
}
This is my ListView:
<ListView ItemsSource="{x:Bind AuthorFacets, Mode=OneWay}"
ItemTemplate="{StaticResource ListViewDataTemplate}"
SelectionMode="Multiple">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
My itemtemplate:
<DataTemplate x:Key="ListViewDataTemplate"
x:DataType="local:ISomeInterface">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Text="{x:Bind read}"/>
<TextBlock Text="{x:Bind num}"/>
</Grid>
</DataTemplate>
Change
<TextBlock Text="{x:Bind read}"/>
<TextBlock Text="{x:Bind num}"/>
to
<TextBlock Text="{x:Bind read, Mode=OneWay}"/>
<TextBlock Text="{x:Bind num, Mode=OneWay}"/>
And implement INotifyPropertyChanged, and it should work. By default, x:Bind is OneTime unlike traditional binding (because performance / memory) so your items in view won't update.
Without INPC? You could possibly try calling Bindings.Update() on the parent view after changing the properties, though you may lose scroll position.
Related
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:
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?
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 :-) .
I've got a shared Flyout defined in my <Page.Resources> as follows:
<Flyout x:Name="InfoFlyout" Opened="{Binding IsOpen,
ElementName=MyListView, Mode=TwoWay}">
<Grid>
<Button Foreground="White" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Help"/>
</StackPanel>
</Button>
</Grid>
</Flyout>
But I get An object reference not set error when compiling, so I used the code from this article (Using Windows 8.1 Flyout control with MVVM) instead.
This seems to circumvent the problem I was having with the above code. Now my shared Flyout code looks like this:
<Flyout x:Name="InfoFlyout"
helpers:FlyoutHelpers.Parent="{Binding ElementName=MyListView}"
helpers:FlyoutHelpers.IsOpen="{Binding IsOpen, Mode=TwoWay}">
<Grid>
<Button Foreground="White" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Help"/>
</StackPanel>
</Button>
</Grid>
</Flyout>
My ListView control (i.e. x:Name="MyListView") is binded to the page's ViewModel i.e. MainPageViewModel. The IsOpen property is defined in the MainViewModel.
Now in my ListView DataTemplate, I want my Flyout to open when I press and hold the ListViewItem or when pressing a button within the ListViewItem:
<DataTemplate>
<Grid FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source={Binding MyImage} />
<Grid Grid.Column="1" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Width="30" Height="30"
Flyout="{StaticResource InfoFlyout}"
content="i">
</Button>
</Grid>
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Holding">
<actions:OpenFlyoutAction />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Grid>
</DataTemplate>
As you can see, I've got the Flyout "attached" to the Grid via:
FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}"
and I've got the same Flyout attached to the button within the ListViewItem itself via:
Flyout="{StaticResource InfoFlyout}"
I've put breakpoints on both my setter and getter for the IsOpen property and when page gets loaded, it does go into the getter but whenever I open or close my Flyout either via Holding or by pressing the 'i' button, it doesn't trigger the method below and therefore it doesn't change the IsOpen property.
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) as defined in the FlyoutHelper class.
The reason I've set my ElementName to MyListView is that I want all my ListViewItem to be binded to the one property i.e. IsOpen as I need to detect whenever a flyout menu is opened irrelevant of which ListViewItem it belongs to.
How can I achieve or resolve this?
UPDATE - 1
The problem of accessing the shared menu has been resolved by using the following:
<Flyout x:Name="InfoFlyout"
helpers:FlyoutHelpers.Parent="{Binding ElementName=MyListView}"
helpers:FlyoutHelpers.IsOpen="{Binding IsOpen, Mode=TwoWay}">
and setting the button to
<Button Width="30" Height="30"
Command="{Binding InformationCommand}"
CommandParameter="{Binding}"
Flyout="{StaticResource InfoFlyout}">
Which is fine and as #ElvisXia mentioned, you can comment out the code in the OnIsOpenPropertyChanged as the positioning is already determined by the button located inside my ListViewItem.
There is however one outstanding problem. A small one btw, but nice if it can be solved. The shared flyout which is attached to the grid itself in the DataTemplate i.e.
<DataTemplate>
<Grid FlyoutBase.AttachedFlyout="{StaticResource InfoFlyout}">
It is being positioning based on the ListViewItem which technically is correct as I'm calling a different piece of code for that one i.e.
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Holding">
<actions:OpenFlyoutAction />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
And the OpenFlyoutAction is defined as follows:
public class OpenFlyoutAction : DependencyObject, IAction
{
public object Execute(object sender, object parameter)
{
FrameworkElement senderElement = sender as FrameworkElement;
FlyoutBase flyoutBase = FlyoutBase.GetAttachedFlyout(senderElement);
flyoutBase.ShowAt(senderElement);
return null;
}
}
Can I somehow stop using the OpenFlyoutAction and use the same code as provided in the article to open my Flyout wherever the user is holding his/her finger on the relevant ListViewItem rather than on top or below the actual ListViewItem?
I understand it's a little bit side track from the original issue which was to share a Flyout by to controls but may as well finish it as it is somehow relevant to the issue.
Thanks.
Change the type of Parent from Button to ListView. To open flyout in particular X,Y position is not possible in WP. You can choose PopUp control instead. Here is a link which i got open the pop up in tapped position. You can use VisualTreeHelper to get PopUp control of tapped ListViewItem
By Using Windows 8.1 Flyout control with MVVM , the author use parent to control where the flyout shows up.
So the author have codes like below(FlyoutHelpers.cs):
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var flyout = d as Flyout;
var parent = (ListView)d.GetValue(ParentProperty);
if (flyout != null && parent != null)
{
var newValue = (bool)e.NewValue;
if (newValue)
flyout.ShowAt(parent);
else
flyout.Hide();
}
}
He use flyout.ShowAt(parent) to let flyout show at parent element. But in your codes you have binded the flyout to the button using:
<Button Width="30" Height="30"
Flyout="{StaticResource InfoFlyout}" content="i">
</Button>
So it is not necessary to let it show at it's parent any more. To fix the problem, you can comment out the statements like below:
private static void OnIsOpenPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
//var flyout = d as Flyout;
//var parent = (ListView)d.GetValue(ParentProperty);
//if (flyout != null && parent != null)
//{
// var newValue = (bool)e.NewValue;
// if (newValue)
// flyout.ShowAt(parent);
// else
// flyout.Hide();
//}
}
Then you will see the flyout shows at the right place.
I made some ExpanderViews and hardcoded everything. That worked and looked nice so I wanted to clean up and only write one ExpanderView in xaml and load everything else with a binding.
As far as I understood I need a ListBox around the whole thing to make it more dynamic?
This is my code so far:
<ListBox ItemsSource="{Binding ContactDe}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<toolkit:ExpanderView Header="{Binding}"
ItemsSource="{Binding LocationName}"
IsNonExpandable="False">
<toolkit:ExpanderView.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding LocationName}" FontFamily="{StaticResource PhoneFontFamilySemiBold}" LineHeight="{StaticResource LongListSelectorGroupHeaderFontSize}" />
</DataTemplate>
</toolkit:ExpanderView.HeaderTemplate>
<toolkit:ExpanderView.ExpanderTemplate>
<DataTemplate>
<TextBlock Text="test" />
</DataTemplate>
</toolkit:ExpanderView.ExpanderTemplate>
<toolkit:ExpanderView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Information}" />
</DataTemplate>
</toolkit:ExpanderView.ItemTemplate>
</toolkit:ExpanderView>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The ContactViewModel-Class:
public class ContactDeViewModel : INotifyPropertyChanged
{
private string _locationName;
public string LocationName
{
get
{
return _locationName;
}
set
{
if (value != _locationName)
{
_locationName = value;
NotifyPropertyChanged("LocationName");
}
}
}
private List<string> _information;
public List<string> Information
{
get
{
return _information;
}
set
{
if (value != _information)
{
_information = value;
NotifyPropertyChanged("Information");
}
}
}
}
And this is where I fill the ContactViewModel:
this.ContactDe.Add(new ContactDeViewModel()
{
LocationName = "Stuttgart",
Information = new List<string>
{
"some text"
}
}
);
this.ContactDe.Add(new ContactDeViewModel()
{
LocationName = "Böblingen",
Information = new List<string>
{
"more text"
}
}
);
I made a SampleViewModel-File where I have:
<vm:MainViewModel.ContactDe>
<vm:ContactDeViewModel LocationName="Location 1" />
<vm:ContactDeViewModel LocationName="Location 2" />
</vm:MainViewModel.ContactDe>
In the preview-window it shows me 2 ExpanderViews with Location 1 and 2. But the same code doesn't work with the emulator or a real device. I don't really understand which Binding-Acces does what. It would already help me a lot if I could see a full example. I googled many tutorials but most only show 1 side, like a xaml without seing how the data is stored.
edit:
Now I edited the viewModel, so it's not a List<string> but a List<Info> with Info only containing string Text. So now I can say ItemsSource="{Binding Text}" which should be only 1 string at a time, right?
As stated in comment to #dellywheel's answer, that you set DataContext this way :
d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}"
that set DataContext for use in design-time only, hence it doesn't work in run-time. To set DataContext with similar approach for use in run-time, you can try this way :
<ListBox ItemsSource="{Binding ContactDe}">
<ListBox.DataContext>
<vm:MainViewModel/>
</ListBox.DataContext>
........
........
</ListBox>
or this way to set DataContext in page level :
<phone:PhoneApplicationPage>
<phone:PhoneApplicationPage.DataContext>
<vm:MainViewModel/>
</phone:PhoneApplicationPage.DataContext>
........
........
</phone:PhoneApplicationPage>
Another suggestion, prefer ObservableCollection rather than List for use along with data binding. ObservableCollection automatically notify view to refresh whenever item added to or removed from collection.
You need to change your bindings slightly
<toolkit:ExpanderView Header="{Binding LocationName}"
ItemsSource="{Binding Information}"
IsNonExpandable="False">
<toolkit:ExpanderView.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontFamily="{StaticResource honeFontFamilySemiBold}" LineHeight="{StaticResource LongListSelectorGroupHeaderFontSize}" />
</DataTemplate>
</toolkit:ExpanderView.HeaderTemplate>
<toolkit:ExpanderView.ExpanderTemplate>
<DataTemplate>
<TextBlock Text="test" />
</DataTemplate>
</toolkit:ExpanderView.ExpanderTemplate>
<toolkit:ExpanderView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</toolkit:ExpanderView.ItemTemplate>
</toolkit:ExpanderView>
Hope that helps
I may be approaching this all wrong, so tell me if you have an alternative suggestions.
I'm making an app for windows RT that will have a bunch of text blocks displayed to the user, for example character stats.
The user will see:
Str: 10
Con: 10
Dex: 10
and so on.
I want them to be able to fill these in, then have a select view values calculated based on the result.
My though was to click an "Edit" button at the top and toggle some text boxes over each editable text block.
When trying to set this up using "Blend for Visual Studio" I can't seem to make a text box that is smaller than 49x34 (much larger than my text blocks).
I was going to find a way to generate a text box for each text block (using its dimensions) on button click, but since they will always be the same and there will be a lot of them I was trying to make them static via blend.
I'm pretty new to XAML, and I can't seem to find a good example of people setting up editable fields like this, so how should I make a bunch of static fields have editable text boxes?
I would create both the TextBox and TextBlock overlays in XAML, and place them directly on top of each other in a Grid, using Horizontal and Vertical alignments to "Center" to ensure that the text is always completely lined up. I would also use static Widths to ensure that the columns line up well.
From there, you can directly bind the Visibility to some boolean "IsEditing" property, to make sure that only one of the controls are shown at a time.
<StackPanel Orientation="Horizontal">
<TextBlock Text="Str: " Width="40" VerticalAlignment="Center" />
<Grid Width="40" VerticalAlignment="Center">
<TextBlock Text="{Binding Strength}"
Visibility="{Binding IsEditing, Converter={StaticResource BooleanToInvisibilityConverter}}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
<TextBox Text="{Binding Strength}"
Visibility="{Binding IsEditing, Converter={StaticResource BooleanToVisibilityConverter}}"
VerticalAlignment="Center"
HorizontalContentAlignment="Center" />
</Grid>
</StackPanel>
Somewhere along the way you'll have to define your "BooleanToVisibility" and "BooleanToInvisiblity" converter resources. I like this implementation by Diedrik Krols. It's nice and simple, with the option to invert.
You might want to use a style for a TextBox, which changes depending on whether or not the "IsReadOnly" property is true or not.
When IsReadOnly is true, you can set the BorderBrush and Background to Transparent, thus making it look like a normal textblock.
In this way, you don't have to overlay TextBlocks and TextBoxes; just use TextBox controls by themselves, and toggle the "IsReadOnly" property when you click the Edit button.
In your resources:
<Style x:Key="MyEditableField" TargetType={x:Type TextBox}>
<Style.Triggers>
<DataTrigger Binding="{Binding IsReadOnly, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Background" Value="Transparent" />
</DataTrigger>
</Style.Triggers>
</Style>
And here's one of your editable fields:
<StackPanel Orientation="Horizontal">
<TextBlock Text="Str: " />
<TextBox Style="{StaticResource MyEditableField}"
Text="{Binding Strength}"
IsReadOnly="{Binding IsEditingDisabled}" />
</StackPanel>
Late answer, but who wants can also create a custom editable textbox, its pretty easy actually here is the code (obviously you can modify it for your own needs)
public class EditableTextBox : TextBox
{
public EditableTextBox()
{
this.BorderBrush = new SolidColorBrush(Colors.Black);
}
protected override void OnTapped(TappedRoutedEventArgs e)
{
this.IsReadOnly = false;
SetEditingStyle();
base.OnTapped(e);
}
protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e)
{
this.IsReadOnly = false;
SetEditingStyle();
base.OnDoubleTapped(e);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
this.IsReadOnly = true;
SetReadonlyStyle();
base.OnLostFocus(e);
}
public void SetReadonlyStyle()
{
this.BorderBrush.Opacity = 0;
this.Background.Opacity = 0;
}
public void SetEditingStyle()
{
this.BorderBrush.Opacity = 1;
this.Background.Opacity = 1;
}
}
Sample:
Tutorial: Full tutorial url
Using a property to toggle edit mode between view AND viewmodel is a bad design approach you should use events and command binding to communicate changes of states like this between view and viewmodel.
Here is an article that describes the principle in an MVVM compliant way:
http://www.codeproject.com/Articles/802385/A-WPF-MVVM-In-Place-Edit-TextBox-Control
Please have a look and tell me what you think.
This builds off of BTownTKD's solution, but as I really do prefer as much WPF of a solution as possible here is a bit of a modification, in my case I'm trying to modify the name of a tab control.
My view model has the following code:
private bool _isEditingName = false;
public bool IsEditingName
{
get
{
return _isEditingName;
}
set
{
_isEditingName = value;
OnPropertyChanged();
}
}
public ICommand StartEditing
{
get
{
return new DelegateCommand(() =>
{
IsEditingName = true;
});
}
}
public ICommand EndEditing
{
get
{
return new DelegateCommand(() =>
{
IsEditingName = false;
});
}
}
Next is my view that has the data template for the tab (not the content just the tab):
<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding ActiveItem}">
<TabControl.ItemTemplate>
<DataTemplate>
<Grid VerticalAlignment="Center">
<TextBlock x:Name="TabName" Text="{Binding Name}" Visibility="{Binding IsEditingName, Converter={StaticResource InvertedBoolToVisConverter}}" VerticalAlignment="Center" HorizontalAlignment="Stretch" TextAlignment="Left">
<TextBlock.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding StartEditing}" />
</TextBlock.InputBindings>
</TextBlock>
<TextBox Text="{Binding Name}" Visibility="{Binding IsEditingName, Converter={StaticResource BoolToVisConverter}}" VerticalAlignment="Center" HorizontalContentAlignment="Stretch" TextAlignment="Left" IsVisibleChanged="TextBox_IsVisibleChanged">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{Binding EndEditing}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding EndEditing}" />
</TextBox.InputBindings>
</TextBox>
</Grid>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
And last but not least, I wanted a double click to put me in edit mode, and to auto focus on the textbox and select all of the content for immediate typing. None of the xaml solutions were as clean as a simple code behind so I finally just decided on adding this to the textbox on visibility changed handler:
private void TextBox_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
var box = sender as TextBox;
if (box != null)
{
if ((bool)e.NewValue)
{
box.Focus();
box.SelectAll();
}
}
}
Out of all of the solutions I found, this was by far my favorite. Thanks everyone for your posts!! Helped me find a really good overall solution to my problem!
I'm trying to create a similar experience as in the ScrollViewerSample from the Windows 8 SDK samples to be able to snap to the items inside a ScrollViewer when scrolling left and right. The implementation from the sample (which works) is like this:
<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
HorizontalAlignment="Left" VerticalAlignment="Top"
VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto"
ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
<StackPanel Orientation="Horizontal">
<Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</StackPanel>
</ScrollViewer>
The only difference with my desired implementation is that I don't want a StackPanel with items inside, but something I can bind to. I am trying to accomplish this with an ItemsControl, but for some reason the Snap behavior does not kick in:
<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
HorizontalAlignment="Left" VerticalAlignment="Top"
VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto"
ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</ItemsControl>
</ScrollViewer>
Suggestions would be greatly appreciated!
Thanks to Denis, I ended up using the following Style on the ItemsControl and removed the ScrollViewer and inline ItemsPanelTemplate altogether:
<Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer Style="{StaticResource HorizontalScrollViewerStyle}" HorizontalSnapPointsType="Mandatory">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Getting snap points to work for bound collections can be tricky. For snap points to work immediate child of ScrollViewer should implement IScrollSnapPointsInfo interface. ItemsControl doesn't implement IScrollSnapPointsInfo and consequently you wouldn't see snapping behaviour.
To work around this issue you got couple options:
Create custom class derived from ItemsControl and implement IScrollSnapPointsInfo interface.
Create custom style for items control and set HorizontalSnapPointsType property on ScrollViewer inside the style.
I've implemented former approach and can confirm that it works, but in your case custom style could be a better choice.
Ok, here is the simplest (and standalone) example for horizontal ListView with binded items and correctly working snapping (see comments in following code).
xaml:
<ListView x:Name="YourListView"
ItemsSource="{x:Bind Path=Items}"
Loaded="YourListView_OnLoaded">
<!--Set items panel to horizontal-->
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<!--Some item template-->
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
background code:
private void YourListView_OnLoaded(object sender, RoutedEventArgs e)
{
//get ListView
var yourList = sender as ListView;
//*** yourList style-based changes ***
//see Style here https://msdn.microsoft.com/en-us/library/windows/apps/mt299137.aspx
//** Change orientation of scrollviewer (name in the Style "ScrollViewer") **
//1. get scrollviewer (child element of yourList)
var sv = GetFirstChildDependencyObjectOfType<ScrollViewer>(yourList);
//2. enable ScrollViewer horizontal scrolling
sv.HorizontalScrollMode =ScrollMode.Auto;
sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
sv.IsHorizontalRailEnabled = true;
//3. disable ScrollViewer vertical scrolling
sv.VerticalScrollMode = ScrollMode.Disabled;
sv.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
sv.IsVerticalRailEnabled = false;
// //no we have horizontally scrolling ListView
//** Enable snapping **
sv.HorizontalSnapPointsType = SnapPointsType.MandatorySingle; //or you can use SnapPointsType.Mandatory
sv.HorizontalSnapPointsAlignment = SnapPointsAlignment.Near; //example works only for Near case, for other there should be some changes
// //no we have horizontally scrolling ListView with snapping and "scroll last item into view" bug (about bug see here http://stackoverflow.com/questions/11084493/snapping-scrollviewer-in-windows-8-metro-in-wide-screens-not-snapping-to-the-las)
//** fix "scroll last item into view" bug **
//1. Get items presenter (child element of yourList)
var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(yourList);
// or var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(sv); //also will work here
//2. Subscribe to its SizeChanged event
ip.SizeChanged += ip_SizeChanged;
//3. see the continuation in: private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
}
public static T GetFirstChildDependencyObjectOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj is T) return depObj as T;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetFirstChildDependencyObjectOfType<T>(child);
if (result != null) return result;
}
return null;
}
private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
{
//3.0 if rev size is same as new - do nothing
//here should be one more condition added by && but it is a little bit complicated and rare, so it is omitted.
//The condition is: yourList.Items.Last() must be equal to (yourList.Items.Last() used on previous call of ip_SizeChanged)
if (e.PreviousSize.Equals(e.NewSize)) return;
//3.1 get sender as our ItemsPresenter
var ip = sender as ItemsPresenter;
//3.2 get the ItemsPresenter parent to get "viewable" width of ItemsPresenter that is ActualWidth of the Scrollviewer (it is scrollviewer actually, but we need just its ActualWidth so - as FrameworkElement is used)
var sv = ip.Parent as FrameworkElement;
//3.3 get parent ListView to be able to get elements Containers
var yourList = GetParent<ListView>(ip);
//3.4 get last item ActualWidth
var lastItem = yourList.Items.Last();
var lastItemContainerObject = yourList.ContainerFromItem(lastItem);
var lastItemContainer = lastItemContainerObject as FrameworkElement;
if (lastItemContainer == null)
{
//NO lastItemContainer YET, wait for next call
return;
}
var lastItemWidth = lastItemContainer.ActualWidth;
//3.5 get margin fix value
var rightMarginFixValue = sv.ActualWidth - lastItemWidth;
//3.6. fix "scroll last item into view" bug
ip.Margin = new Thickness(ip.Margin.Left,
ip.Margin.Top,
ip.Margin.Right + rightMarginFixValue, //APPLY FIX
ip.Margin.Bottom);
}
public static T GetParent<T>(DependencyObject reference) where T : class
{
var depObj = VisualTreeHelper.GetParent(reference);
if (depObj == null) return (T)null;
while (true)
{
var depClass = depObj as T;
if (depClass != null) return depClass;
depObj = VisualTreeHelper.GetParent(depObj);
if (depObj == null) return (T)null;
}
}
About this example.
Most of checks and errors handling is omitted.
If you override ListView Style/Template, VisualTree search parts must be changed accordingly
I'd rather create inherited from ListView control with this logic, than use provided example as-is in real code.
Same code works for Vertical case (or both) with small changes.
Mentioned snapping bug - ScrollViewer bug of handling SnapPointsType.MandatorySingle and SnapPointsType.Mandatory cases. It appears for items with not-fixed sizes
.