WPF MVVM DataTemplates - View Not updating - c#

I'm working on a digital signage WPF application and the basic structure is setup as follows:
I have a single view with a grid in it bound to a playlist object. The playlist contains panes, the panes contain playlist items, and the playlist items each contain a content item. I use DataTemplates to build the view for each piece, e.g.
<!-- This template represents a single content pane, no real display here, just empty space with a height and width -->
<DataTemplate DataType="{x:Type entities:ContentPane}">
<ContentControl
Content="{Binding CurrentPlaylistItem, Mode=OneWay}"
Height="{Binding Height, Mode=OneTime}"
Width="{Binding Width, Mode=OneTime}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
/>
</DataTemplate>
<!-- This is a playlist item which is contained within a ContentPane.
This also has no real display, just a placeholder for the content item, and will likely contain some transition information-->
<DataTemplate DataType="{x:Type entities:PlaylistItem}">
<inf:TransitionableContentControl Content="{Binding ContentItem}"></inf:TransitionableContentControl>
</DataTemplate>
<!-- image item template
the path can be any valid uri e.g. http://... or file://... -->
<DataTemplate DataType="{x:Type contentTypes:ImageContentType}">
<Grid Background="Transparent" x:Name="ImageType">
<Image Source="{Binding Bitmap, Mode=OneTime}"></Image>
<inf:BusinessAd
ContainerHeight="{Binding ImageHeight, Mode=OneTime}"
ContainerWidth="{Binding ImageWidth, Mode=OneTime}"
Visibility="{Binding AdText, Converter={StaticResource _VisibilityConverter}, Mode=OneWay}"
Text="{Binding AdText.Text, Mode=OneTime}"
AdFontSize="{Binding AdText.TextStyle.FontSize}"
AdFontFamily="{Binding AdText.TextStyle.FontFamily}">
<ContentControl
Content="{Binding AdText, Mode=OneTime}"
ContentTemplate="{StaticResource BusinessAdTextTemplate}">
</ContentControl>
</inf:BusinessAd>
</Grid>
</DataTemplate>
As the datacontext changes, the content in each pane transitions from one item to the next. I'm running into a problem where the user is placing the same item in a pane twice in a row, be it a video, image, etc. What happens is the change goes undetected and the UI does not update. In the case of a video, it freezes on the last frame of the first video and the whole application hangs.
I have tried doing an OnPropertyChanged("ContentItem") within the PlaylistItem class, tried setting Shared="False" on my data templates, I tried changing properties on the ContentItem object and raising PropertyChanged events, nothing seems to work. I turned on tracing on the databinding to see what was happening and it all appears to be working correctly. When I change properties on the ContentItem it shows a new hash for the new item, but no change on the UI.
Within the TransitionableContentControl in the PlaylistItem, the OnContentChanged override is never hit when going from one content item to the same one. If I swap that control out with a regular ContentControl, no change.

Former Employee is correct about the diagnosis. Furthermore, unless you manage to re-assign the inner datacontexts your templates will not restart from the beginning, as you have noticed. You will have to assign your CurrentPlaylistItem to some default empty item first to reset stuff all the way through. To avoid races and nasty flicker, do it on a dispatcher priority higher than UI rendering. Try this:
// CurrentPlaylistItem = pli ; -- not good
Application.Current.Dispatcher.Invoke (new Action (() =>
{
// clear item
// null doesn't work, because TransitionableContentControl's
// inner ContentControl would be empty and the brush created
// from it would be a null brush (no visual effect)
CurrentPlaylistItem = new PlaylistItem () ;
this.OnPropertyChanged ("CurrentPlaylistItem") ;
Application.Current.Dispatcher.Invoke (new Action (() =>
{
// set new item, possibly same as old item
// but WPF will have forgotten that because
// data bindings caused by the assignment above
// have already been queued at the same DataBind
// level and will execute before this assignment
CurrentPlaylistItem = pli ;
this.OnPropertyChanged ("CurrentPlaylistItem") ;
}), DispatcherPriority.DataBind) ;
})) ;

The issue here is the ContentControl (or rather the underlying DependencyProperty framework) doesn't fire OnContentChanged when .Equals is true for old and new Content. One workaround is to have your own dependency property and transition on CoerceValue. I'll email you the code and you can post it here.

Related

UWP ListView only displays data sometimes

My data from an ObservableCollection only sometimes displays on my ListView. If I restart the app, the data displays fine. Sometimes when I navigate away from the page and go back, the data will sometimes display and other times not. It seems to be random.
Here is my XAML code:
<ScrollViewer Grid.Row="2" Margin="0,42,0,0">
<Grid>
<ListView ItemsSource="{x:Bind collection, Mode=OneWay}" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" IsItemClickEnabled="True" SelectionChanged="MySelectionChanged" Visibility="Visible">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ObjectName">
<TextBlock Text="{x:Bind Data0, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView ItemsSource="{x:Bind collection, Mode=OneWay}" HorizontalAlignment="Left" Margin="375,0,0,0" VerticalAlignment="Top" SelectionMode="None" Visibility="Visible">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ObjectName">
<TextBlock Text="{x:Bind Data1, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ScrollViewer>
I tried doing a Debug.WriteLine() on all the data that should be displayed, and the debug output successfully prints it every time. There's only a problem when it comes to displaying this data on the ListView.
EDIT: Here's the corresponding xaml.cs code:
private ObservableCollection<ObjectName> collection;
private List<ObjectName> sorted;
private ObjectName clicked = new ObjectName();
public MainPage()
{
GetSave();
this.InitializeComponent();
}
private async void GetSave()
{
DataStorage ds = new DataStorage();
await ds.DeserializeObjectAsync();
collection = ds.ObjectName;
if (collection != null)
{
sorted = new List<ObjectName>(collection);
sorted.Sort((x, y) => string.Compare(x.Data0, y.Data0));
collection = new ObservableCollection<ObjectName>(sorted);
}
}
I tried InitializeComponent() before and after the logic, and it gives the same result.
Based on the xaml.cs code you provided, the problem looks like this:
First, you are not awaiting an async call.
Second, you are replacing the ObservableCollection.
The solution:
Don't call async code from your page's constructor. Async calls are
called async for a reason and trying to 'hide' their true nature by
omitting the await keyword when calling them won't work. Change GetSave()'s
return value to Task, override the OnNavigatedTo() method (read more
about it in it's documentation) of MainPage and await GetSave()
inside that. This alone won't solve your problem though.
Never replace an ObservableCollection. I remember banging my head on the
wall multiple times because of this. What you need to understand is
that when you are binding to an object in XAML, a dedicated Binding
object is created that links the source (in your case, the
ObservableCollection) and the target (the ListView) together. In
your code, initially 'collection' is set to null. When your MainPage
is created, the Binding object is created as well and it binds that
null value to your ListViews' Source property. Later, when your
async initialzation code is finished, you replace that null value
with an actual ObservableCollection, but the ListViews are not
notified about that, they are only looking for changes in the
collection's items that they are bound to, they are not prepared for
handling the situation when the collection itself is swapped
under them. So what you can do to fix this problem is: only create
your ObservableCollection instance in MainPage's constructor (or at
declaration - matter of taste in this case) and in GetSave() first
call collection.Clear() and then add your items to it with
collection.Add().
What currently happening in your code is that in some cases you are replacing the ObservableCollection before the Binding object is created and sometimes after that, so that's why it looks like you app's behavior is totally random - because actually it is. :) By adding the modifications I suggested above, you'll make sure that the Binding object is bound to the ObservableCollection you created in the constructor (empty at the time of the binding), and then you are initializing that collection after MainPage is already loaded, so your ListViews are getting notified about the changes.

How to get rid of double selection thing in ListView (XAML)

I have a ListView and I bind to an ObservableCollection<Folder> and when I hover over a ListViewItem, there is a second selection thing appearing underneath (or ontop of) the item text which prevents me from being able to "activate" the selected item because it doesn't appear to be receiving my click.
As you can probably see, the structure is:
ListView > ItemTemplate > DataTemplate > ListViewItem. But I'm guessing I have that double-selection thing because there are basically 2 "item templates" (DataTemplate and ItemTemplate). But if I get rid of DataTemplate, it throws a runtime error. If I get rid of ItemTemplate, it throws an error. I can't win. How do I get rid of this thing?
Update:
This gives me the desired effect:
<StackPanel x:Name="folderContainer" HorizontalAlignment="Left" VerticalAlignment="Stretch" Width="175" Background="Khaki" Margin="0, 18, 0, 0">
<ListView x:Name="folderList" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Folder}" BorderBrush="{x:Null}" BorderThickness="0">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="folderItem" Text="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
With the ListViewItem you where including a specialized wrapper primarily used the ListView to contain unknown item(s) presented by the binding of the ItemsSource when there is default template used. It helps to show that unknown item on the screen, its the purpose of the ListViewItem. For example if someone bound a list of strings, the strings have no xaml style on hover, hence there needs to be a container control to achieve all things graphical in those situations.
Why the two Shadows?
The actual ListViewItem is at its heart (or actually its property), a content control containing what I surmise you had was a textbox control. That is now two controls thanks to the DataTemplate you provided.
Hence you had a wrapper, with a content containing a texbox control. So there are two things which take up space, one is the ListViewItem and the other is the textbox. By hovering over each item, each's style kicked off to show the padding of the control as a selectable region in the Zorder fashion whereas both are seen.
Nothing more nothing less.
Since you knew what is to go into the content control, there was no need to use the ListViewItem wrapper, it was redundant, and you used the actual textbox; hence with only it's style to show the padding of the single control on hover.

Forcing a ListBox to re-render

Background:
I have a ListBox containing items defined by DataTemplates. Right now, if an object in the list has the property IsEditable set to true, the item's property information will be displayed inside of textboxes (via DataTemplate change), instead of textblocks (so the user can edit the content of that list item)
IsEditable is toggled on/off by a button inside of each list item. I have been told that we need to keep the state of all objects consistent, which means I can't just rebind the ItemsSource and lose everything.
Currently, I'm using this to re-render:
this.lbPoints.Dispatcher.Invoke(DispatcherPriority.Render, new Action(() => { }));
Question:
The aforementioned code snippet KIND OF does its job. By "kind of", I mean, it does eventually cause my data to become re-rendered, but only when I scroll to the bottom of the list and then scroll back up to the item i'm trying to re-render.
1) How can I re-render the data immediately without having to scroll around to get it to show up?
The guys commenting are right that you're going about this the wrong way... there is rarely a need to force a ListBox to re-render. You're probably causing yourself some additional grief trying to switch the DataTemplates (although it is possible). Instead of that, think about data binding the TextBox.IsReadOnly property to your IsEditable property:
<TextBox IsReadOnly="{Binding IsEditable}" Text="{Binding Text}" />
Another alternative is to use a BooleanToVisibilityConverter to show a different Grid in your DataTemplate when your IsEditable property is true. Unfortunately, that Converter doesn't have an inverse operation, so you could create an IsNotEditing property to bind to the Grid in the DataTemplate that is originally displayed. I'm not sure if that's clear... see this example:
<DataTemplate DataType="{x:Type YourPrefix:YourDataType}">
<Grid>
<Grid Visibility="{Binding IsNotEditing, Converter={StaticResource
BooleanToVisibilityConverter}}">
<!-- Define your uneditable UI here -->
</Grid>
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
BooleanToVisibilityConverter}}">
<!-- Define your editable UI here -->
</Grid>
</Grid>
</DataTemplate>
You could also define your own BooleanToVisibilityConverter class that has an IsInverted property, so that you can just use the one IsEditing property. You'd need to declare two Converters still, like this:
<Converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Converters:BoolToVisibilityConverter x:Key="InvertedBoolToVisibilityConverter"
IsInverted="True" />
Then your XAML would be like this:
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
InvertedBoolToVisibilityConverter}}">
<!-- Define your uneditable UI here -->
</Grid>
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
BoolToVisibilityConverter}}">
<!-- Define your editable UI here -->
</Grid>

WPF Style Combobox with two columns in dropdown

I've looked around, found some things and now stuck on a combobox with two columns displayed in the drop-down area. I have a xaml themes available and the combobox "Style" is defined and works well throughout as expected, so that part is ok.
Now, I have a combobox that I need to have display two values, think of it as State Abbreviation and State Name for the drop-down, coming from a DataTable.DefaultView binding source for the items.
If I have
<my:cboStates TextSearch.TextPath="StateAbbrev">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" TextSearch.Text="{Binding Path=StateAbbrev}">
<TextBlock Text="{Binding Path=StateAbbrev}"/>
<TextBlock Text="{Binding Path=FullStateName}" Margin="10 0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</my:cboStates>
this works. Now, how/where I'm stuck... Now, I want this same functionality on say 5 different forms and all to have the same content displayed, and if ever changed (not this, but for other multi-column comboboxes), I don't want to have to keep putting this directly in the form's XAML file.
I was hoping to put into a Theme's Resource Dictionary file and just keep reusing that "style" over and over. Makes sense. However, when I do, and the binding is to the data table, the only results I get when trying to do as a Style is the dropdown shows values of
System.Data.DataRowView
System.Data.DataRowView
System.Data.DataRowView
System.Data.DataRowView
instead of the actual 2 columns.
Here is what I have in the "theme" resource dictionary.
<DataTemplate x:Key="myStateComboTemplate" >
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Path=StateAbbrev}"/>
<TextBlock Text="{Binding Path=FullStateName}"/>
</StackPanel>
</DataTemplate>
<Style x:Key="StyleMyStatesCombobox" TargetType="{x:Type ComboBox}"
BasedOn="{StaticResource MyOtherWorkingComboBoxStyle}" >
<Setter Property="TextSearch.TextPath" Value="{Binding Path=StateAbbrev}" />
<Setter Property="ItemTemplate" Value="{StaticResource myStateComboTemplate}" />
</Style>
So, If I have TWO instances my "cboStates" class created on the form, and set one to the explicit styling listed first, and the SECOND based on the "Style" setting, the second one fails by only showing the repeated System.Data.DataRowView entries, not the actual data content.
What am I missing.
So, to clarify what I'm looking for...
States... ex data
AL Alabama
AK Alaska
AZ Arizona
AR Arkansas
CA California
CO Colorado
CT Connecticut
DE Delaware
I want the combobox to be displaying the abbreviated
AL, AK, AZ, etc and narrower combobox. This will ALSO be the "SelectedValue" upon return.
The actual Dropdown would present the data as listed above showing BOTH the abbreviation AND the long description of the state.
Sample of desired combobox
FINALLY got it working... and for those attempting similar. Since I was trying to have a standard "class" instance that could be used throughout, but not wanting to explicitly hard-reference the XAML in each page, part of the styling had to be handled during the actual in-code class instance.
Since I don't exactly know how/when where .net framework builds out all its controls, style assigments, etc, I was getting frustrated that it would work if direct from xaml, but fail when in code. So, I ended up FORCING the item template AND TextSearch.TextPath values in the code. Here's a short snippet of the class
public class myStatesCombo : ComboBox
{
public myStatesCombo()
{
Loaded += myAfterLoaded;
}
protected static DataTable myTableOfStates;
public void myAfterLoaded()
{
if( myTableOfStates == null )
myTableOfStates = new DataTable();
CallProcedureToPopulateStates( myTableOfStates );
ItemsSource = myTableOfStates.DefaultView;
// AFTER the object is created, and all default styles attempted to be set,
// FORCE looking for the resource of the "DataTemplate" in the themes.xaml file
object tryFindObj = TryFindResource("myStateComboTemplate" );
if( tryFindObj is DataTemplate )
ItemTemplate = (DataTemplate)tryFindObj;
// NOW, the CRITICAL component missed in the source code
TextSearch.SetTextPath( this, "StateAbbrev" );
}
}
Now, a special note. In the routine I used to populate the DataTable, I pre-check if the table exists or not. First time in, I create the table. If I need to re-populate it, if I just keep doing a "new DataTable" each time, it blows away the data/item template bindings. To PREVENT that, I would then do
if( myTableOfStates.Rows.Count > 0 )
myTableOfStates.Rows.Clear();
THEN, I call my
Sqlexecute call to query from the database (DataAdapter) and Fill() the datatable.
So, now all appears to be populated correctly, bindings for display and textsearch complete and ready to go.

Since a selection list, how can I set the navigation sequentially?

I'm starting a new project in WPF and am now looking into using Prism. For now I'm simply trying to set up the navigation of the application using Prism. Unfortunately, my lack of experience with the framework makes it a bit difficult to get started.
To be more precise about my first challenge I have an application with a "navigation/menu" region and a "main" region.
In "navigation/menu" region, I have several checkboxes, in this case we have four of them, which represents a sequential navigation. I.E. we've selected View 2 and View 4.
So, when the user click Start, in "main" region must appear each view selected in that order. Check the below image, View 2 is first. Then when the user press next, must show View 4.
I mean on a more structural level..
if I could only get through the first steps..
Prism support TabControl Region Adapter, navigation can be done using standard requestNavigation method.
You need add all your tab content using Region.Add method to the region in your module's init phase.
view:
<TabControl prism:RegionManger.RegionName="tabRegion" />
C# code:
IRegionManager manager;
manager.Regions["tabRegion"].Views.Add(Container.Resolve(typeof(YourViewType)));
In your viewModel, you should write you navigation command:
public void NextView() {
regionManager.RequestNavigation("tabRegion", new Uri("YourViewType", UriKind.Relative));
}
bind to your "next" button:
<Button Command="{Binding NextViewCommand}" />
If you want to control whether user can navigate to next page, you can implement INavigationAware interface.
If you don't want lost data between navigation, you can make your view model has ContainerMangedLifeCycle or implement IsNavigationTarget method to return true.
Sorry for untested code sample, but you should get the point.
Create a class named ViewVM with a property IsSelected. Must implement INotifyPropertyChanged.
Add an ObservableCollection<View> named Views to your datacontext. Populate it with new instances of ViewVM.
Put an ItemsControl in your Window, with ItemsSource set to Views. The DataTemplate for the ItemsControl items should contain a CheckBox (with IsChecked bound to IsSelected) and a Label.
Add a TabControl to your Window, with ItemSource set to Views. Add a Style for TabItem such that TabItems are only visible if IsSelected is true.
Following the above steps will give you a window containing a list of views with checkboxes, as you requested, and a TabControl displaying only the selected views. Below is the XAML (I have tested this):
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=Views}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"></CheckBox>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TabControl ItemsSource="{Binding Path=Views}">
<TabControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</Grid>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.Resources>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected}" Value="False">
<Setter Property="Visibility" Value="Collapsed"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Resources>
</TabControl>
</StackPanel>
This addresses the structural/design aspect and should give you a good start to creating your solution - you'll also need to create a custom control to use instead of the TabControl. Instead of having tabs, your custom control should contain Next and Previous buttons to navigate between views.

Categories

Resources