UWP XAML Databinding working on one text box, but not another - c#

I'm running into a peculiar issue, where data-binding two text boxes to the same object works just fine, but after I load data back into my program via XML deserialization the binding fails on one of those text boxes.
I use a splitview to build a little hamburger navigation menu and load my pages into a frame
Here is the related XAML in my MainPage.xaml
<TextBlock Name="PlayerGoldTextBlock"
RelativePanel.RightOf="PlayerNameTextBlock"
RelativePanel.AlignVerticalCenterWithPanel="True"
Foreground="Gold"
FontSize="28"
Text="{x:Bind myGame.Game.Player.Gold, Mode=OneWay}" />
<TextBlock Name="RemainingTurnsTextBlock"
RelativePanel.RightOf="PlayerGoldTextBlock"
RelativePanel.AlignVerticalCenterWithPanel="True"
Foreground="Red"
Text="{x:Bind myGame.Game.RemainingTurns, Mode=OneWay}" />
<SplitView.Content>
<Frame Name="MyFrame"></Frame>
</SplitView.Content>
In the page I have loaded into the frame:
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Gold:" HorizontalAlignment="Left" FontSize="16" Margin="0,0,10,0"/>
<TextBlock Text="{x:Bind myGame.Game.Player.Gold, Mode=OneWay}" HorizontalAlignment="Left" FontSize="16" Margin="0,0,10,0"/>
</StackPanel>
in MainPage: The PlayerGoldTextBlock fails to bind correctly after I load, but the RemainingTurnsTextBlock works fine
in my frame: x:Bind myGame.Game.Player.Gold seems to rebind just fine after I load.
Another thing, which may be related, is that upon loading I see the RemainingTurnsTextBlock update immediately, but the (working) player gold in the frame doesn't until I navigate to a different frame and then back
here is my deserialization code, in case that matters:
async public void LoadGame()
{
var filename = "testsave.xml";
var serializer = new XmlSerializer(typeof(GameModel));
StorageFolder folder = ApplicationData.Current.LocalFolder;
StorageFile file = await folder.GetFileAsync(filename).AsTask().ConfigureAwait(false);
Stream stream = await file.OpenStreamForReadAsync().ConfigureAwait(false);
GameModel myGame;
using (stream)
{
myGame = (GameModel)serializer.Deserialize(stream);
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
this.Player = myGame.Player;
this.Locations = myGame.Locations;
this.RemainingTurns = myGame.RemainingTurns;
});
}
}
here are some of the properties from my GameModel
public Player Player { get; set; }
public int MaxTurns { get; set; }
private int remainingTurns;
public int RemainingTurns
{
get { return remainingTurns; }
set { SetProperty(remainingTurns, value, () => remainingTurns = value);}
}
here is the gold property in player
private int gold;
public int Gold
{
get { return gold; }
set {SetProperty(gold, value, () => gold = value);}
}
here is setproperty
protected bool SetProperty<T>(T currentValue, T newValue, Action DoSet,
[CallerMemberName] String property = null)
{
if (EqualityComparer<T>.Default.Equals(currentValue, newValue)) return false;
DoSet?.Invoke();
RaisePropertyChanged(property);
return true;
}
Edit to clarify problem statement based on XY problem comment:
I have things bound to Game.Player.Gold
On launch they work fine
After doing XML deserialization and loading a saved state, some of the binding works when I navigate away and come back, but some dies completely
Binding doesn't seem to like when I change the player instance or something, because it can't drill down into where the gold property is. Its weird to me that one textbox in the frame can rebind after i navigate, but one in the mainwindow can't
I found a fix for this, using a setter on Game.Player directly, in addition to Game.Player.Gold, but I see some similar problems cropping up for other properties.
Do I have some misunderstanding of how these things see one another?

You are not updating myGame.Game.Player.Gold live, so it will fetch the details from the Player class when the page reloads for the frame, but is missing the details to update it instantly upon change as RemainingTurns does. There may be an issue with accessors between the Frame and the Main Window.
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
this.Player = myGame.Player;
this.Locations = myGame.Locations;
this.RemainingTurns = myGame.RemainingTurns;
// You need to get the value for Player.Gold
});

I solved this by adding a Setter to the Player property in my Game
public Player Player
{
get { return player; }
set { SetProperty(player, value, () => player = value); }
}
This seems to fire off an event for anything subscribing to the properties of player that the player has been changed.
However, it didn't fix all the databinding issues I was seeing post load for different Player properties. For example, an ObservableCollection property still doesn't seem to bind right until I navigate frames.
It works fine when I start the game, but after I load in data from XML it still requires a navigation to populate correctly even with this fix added in.
I'm not sure if this is the right fix and I have a second issue or if this fix is just a weird bandaide for part of the issue. It might be a misunderstanding on the databinding aspect or on the deserialization aspect that I'm not getting.

Related

Avalonia UserControl checking if it is visible before before acting on a timer

I have a loading overlay (with the View inheriting from UserControl and the ViewModel from ViewModelBase) that I display over the current window by putting using a <Grid> and having the regular controls in a <StackPanel> and then the loading screen after it in a <Border>, binding the <Border>'s IsVisible property to control the display of the overlay.
<Window ...>
<Grid>
<StackPanel>
<!-- Window controls here -->
</StackPanel>
<Border Background="#40000000"
IsVisible="{Binding IsLoading}">
<views:LoadingScreenView />
</Border>
</Grid>
</Window>
In the LoadingScreenViewModel I use an HttpClient to download a JSON file to parse and display on the loading overlay.
It is refreshed in the LoadingScreenViewModel every 10 seconds by using a timer
private IObservable<long> timer = Observable.Interval(TimeSpan.FromSeconds(10),
Scheduler.Default);
and then subscribing to it in the ViewModel's constructor
public LoadingScreenViewModel()
{
LoadingText = "Loading...";
timer.Subscribe(async _ =>
{
var json = await _httpClient.GetStringAsync(...);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
LoadingText = dict["result"];
});
}
The problem is that since I include the LoadingScreenView in the window, this timer is firing every ten second, even when the loading overlay isn't displayed.
Is there any way to check if the overlay itself is visible during that Subscribe(), passing the IsLoading property to the LoadingScreenViewModel, or creating and destroying the View every time it is used?
I was able to achieve this by adding a bool isVisible property to the LoadingScreenViewModel, having the view inherit from ReactiveUserControl<LoadingScreenViewModel>, and per the discussion at https://github.com/AvaloniaUI/Avalonia/discussions/7876 I achieved this in code-behind by subscribing to changes in the view's TransformedBounds property and determining if the view is visible based on if TransformedBounds is null or not.
public LoadingScreenView()
{
InitializeComponent();
this.WhenAnyValue(v => v.TransformedBounds)
.Subscribe(x => ViewModel.isVisible = x is not null);
}

How to Update ComboBox ItemsSource Without Flickering

I am struggling with an update to a ComboBox that previously worked. I originally had its ItemsSource bound to a read-only ObservableCollection<char> property in the ViewModel. When the user instigates changes (which is done with mouse strokes, so dozens of times per second in some cases), the get rebuilds the collection from the Model and returns it.
When I changed to my own object in the ObservableCollection, the ComboBox started flickering during updates. I'm not sure what's going wrong. Here's the code that works, starting with the XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}' />
ViewModel:
public ObservableCollection<char> FromBins
{
get
{
ObservableCollection<char> tempBins = new ObservableCollection<char>();
foreach (var item in Map.BinCounts)
{
tempBins.Add(item.Key);
}
return tempBins;
}
}
I simply raise a property change with every mouse movement and the interface works as expected (there is some other logic to ensure the SelectedItem is valid).
To make the interface more useful, I decided to add more information to the ComboBox, using my own class:
public class BinItem : IEquatable<BinItem>
{
public char Bin { get; set; }
public SolidColorBrush BinColor { get; set; }
public string BinColorToolTip { get {...} }
public BinItem( char bin )
{
Bin = bin;
BinColor = new SolidColorBrush(BinColors.GetBinColor(bin));
}
public bool Equals(BinItem other)
{
return other.Bin == Bin ? true : false;
}
}
If I swap char out for BinItem in the working code ViewModel I get flickering as the mouse is moved. Here is the updated XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}'>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" ToolTip='{Binding BinColorToolTip}'>
<Rectangle Fill='{Binding BinColor}' Width='10' Height='10' HorizontalAlignment='Center' VerticalAlignment='Center' Margin='0,0,4,0' Stroke='#FF747474' />
<TextBlock Text="{Binding Bin}" Width='16' />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I have tried numerous things, including but not limited to:
-Using a List instead of the ObservableCollection but even though the Get fires every time and returns the correct collection of items, the interface does not always update (though the flickering disappears).
-Leaving all possible bins in the items source and adding a Visibility property to the BinItem class that I bound to (couldn't get it to update).
I suspect I am doing something fundamentally wrong, but no amount of searching SO or otherwise has helped thus far. Any help is appreciated.
I was able to solve this using the ideas from Clemens and Chris. Not sure if this is the most elegant solution, but it works as intended with no measurable performance hit.
Instead of replacing the collection with each refresh, I go through the logic of finding out what's changed (with each update there could be an addition AND a removal simultaneously). Code below:
private ObservableCollection<BinItem> _FromBins = new ObservableCollection<BinItem>();
public ObservableCollection<BinItem> FromBins
{
get
{
if (_FromBins.Count > 0)
{
List<char> BinsToRemove = new List<char>();
foreach (var item in _FromBins)
{
if (!Map.BinCounts.ContainsKey(item.Bin))
{
BinsToRemove.Add(item.Bin);
}
}
foreach (var item in BinsToRemove)
{
_FromBins.Remove(new BinItem(item));
}
}
foreach (var item in Map.BinCounts)
{
if (!_FromBins.Contains(new BinItem(item.Key)) && item.Value > 0) {
_FromBins.Add(new BinItem(item.Key));
}
}
return _FromBins;
}
}
Hope this can help someone else too.

Universal Windows Platform - UI not updating when returning from different page

I'm learning Universal Windows Platform and I'm currently analysing music player using SoundCloud API from this site. Link to the github project is at the very bottom of the page. To get this project to work variable SoundCloudClientId from App.xaml.cs should be filled in (I used client id from previous example on the same page, not sure if I can paste that).
When application starts the NowPlaying page is loaded and changing tracks causes UI to update accordingly. The problem is when I navigate to any other page and return back to NowPlaying. I can still change music using buttons, but UI doesn't change (song title, album title etc.).
Important parts of the code:
NowPlaying.xaml
<ImageBrush x:Name="albumrtImage" ImageSource="Assets\Albumart.png" Stretch="UniformToFill" />
<TextBlock x:Name="txtSongTitle" Grid.Row="0" HorizontalAlignment="Center" Text="Song Title " FontSize="25" Foreground="White" Style="{StaticResource HeaderTextBlockStyle}" TextTrimming="WordEllipsis" />
<TextBlock x:Name="txtAlbumTitle" Grid.Row="0" Text="Label " HorizontalAlignment="Center" FontWeight="Light" FontSize="20" Foreground="#9799a5" Style="{StaticResource BodyTextBlockStyle}" TextTrimming="WordEllipsis"/>
NowPlaying.xaml.cs
async void BackgroundMediaPlayer_MessageReceivedFromBackground(object sender, MediaPlayerDataReceivedEventArgs e)
{
TrackChangedMessage trackChangedMessage;
if (MessageService.TryParseMessage(e.Data, out trackChangedMessage))
{
// When foreground app is active change track based on background message
await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var songIndex = GetSongIndexById(trackChangedMessage.TrackId);
if (songIndex >= 0)
{
var song = App.likes[songIndex];
LoadTrack(song); //Update UI
}
});
return;
}
BackgroundAudioTaskStartedMessage backgroundAudioTaskStartedMessage;
if (MessageService.TryParseMessage(e.Data, out backgroundAudioTaskStartedMessage))
{
backgroundAudioTaskStarted.Set();
return;
}
}
private async void LoadTrack(SoundCloudTrack currentTrack)
{
try
{
//Change album art
string albumartImage = Convert.ToString(currentTrack.artwork_url);
if (string.IsNullOrWhiteSpace(albumartImage))
{
albumartImage = #"ms-appx:///Assets/Albumart.png";
}
else
{
albumartImage = albumartImage.Replace("-large", "-t500x500");
}
//Next 3 lines when pages were switched don't cause UI to update
albumrtImage.ImageSource = new BitmapImage(new Uri(albumartImage));
txtSongTitle.Text = currentTrack.title;
txtAlbumTitle.Text = Convert.ToString(currentTrack.user.username);
}
catch (Exception ex)
{
MessageDialog showMessgae = new MessageDialog("Something went wrong. Please try again. Error Details : " + ex.Message);
await showMessgae.ShowAsync();
}
}
After navigating from NowPlaying->Me->NowPlaying and clicking next the track changes, but UI doesn't update as seen on the screen below:
UI problem
I'm trying to reproduce the problem on a simple example, but without any luck. What could cause this issue? Any help is appreciated.
I've found the solution. The problem was with cache. The NowPlaying page required putting following property:
NavigationCacheMode="Required"
I just want to add that you can also check navigation mode in OnNavigatedTo method. It can be helpful when for instance you would like to refresh the data when returning to the selected page.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(e.NavigationMode == NavigationMode.Back)
{
//refresh the data here...
}
}

LongListSelector and DataTemplateSelector

I'm using the LongListSelector to realize List or Grid display for my items. For this, I created a DataTemplateSelector and I change the LayoutMode property at runtime. This is working but there seems to be an issue with the DataTemplateSelector. If I initially launch the page, the DataTemplateSelector is called three times for my three items. When I navigate to another page (settings page to change the LayoutMode) and then back, the DataTemplateSelector is just called two items but there are still three items.
DataTemplateSelector:
public abstract class DataTemplateSelector : ContentControl
{
public virtual DataTemplate SelectTemplate(object item, DependencyObject container)
{
return null;
}
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
ContentTemplate = SelectTemplate(newContent, this);
}
}
ItemViewModeTemplateSelector:
public class ItemViewModeTemplateSelector: DataTemplateSelector
{
public DataTemplate ListViewModeTemplate
{
get;
set;
}
public DataTemplate GridViewModeTemplate
{
get;
set;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
ViewMode viewMode = ViewMode.Grid;
// Get ViewMode from IsolatedStorageSettings...
switch (viewMode)
{
case ViewMode.Grid:
return GridViewModeTemplate;
case ViewMode.List:
return ListViewModeTemplate;
}
return base.SelectTemplate(item, container);
}
}
MainPage.xaml:
<phone:LongListSelector x:Name="ItemLongListSelector" ItemsSource="{Binding Items}" LayoutMode="Grid" GridCellSize="222,222">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<common:ItemViewModeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<common:ItemViewModeTemplateSelector.GridViewModeTemplate>
<DataTemplate>
<StackPanel Margin="12,12,0,0" Background="{Binding Color, Converter={StaticResource ColorToBrushConverter}}">
<!-- Content -->
</StackPanel>
</DataTemplate>
</common:ItemViewModeTemplateSelector.GridViewModeTemplate>
<common:ItemViewModeTemplateSelector.ListViewModeTemplate>
<DataTemplate>
<StackPanel>
<!-- Content -->
</StackPanel>
</DataTemplate>
</common:ItemViewModeTemplateSelector.ListViewModeTemplate>
</common:ItemViewModeTemplateSelector>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
This is the display when I initially launch the page:
Then I navigate to another page and then back:
EDIT: I prepared a sample project for this issue. It should run without problems.
Project: http://sdrv.ms/1cAbVxE
I haven't got the solution but maybe a clue for someone who will solve the problem.
I think the problem is with LongListSelector.UpdateLayout() method - when it's fired for the first time there are no items to which LLS was bound - OnChangeMethod is called that many times as the Itemsource.Count. But when we leave the page and go back - LLS is Updated and method is called ommiting the middle element.
It means it works for even number of items - OnChangeMethod is called correct number of times, But for odd number of items - it's called numer of items - 1.
The second thing is why it's called at all - when there are no changes.
I also add a code to work on which (very simple).
I've done something similar with my app, but allowed the user to choose the LayoutMode of LLS using an Appbar button. I basically change the LongListSelector.LayoutMode and then it's ItemTemplate in code and the LLS automatically refreshes itself. I'm not sure if this will help, but here's my code.
private void layoutModeButton_Click(object sender, EventArgs e)
{
ApplicationBarIconButton layoutModeButton = (ApplicationBarIconButton)ApplicationBar.Buttons[0];
if (MainLongListSelector.LayoutMode == LongListSelectorLayoutMode.Grid)
{
MainLongListSelector.LayoutMode = LongListSelectorLayoutMode.List;
MainLongListSelector.ItemTemplate = this.Resources["ListListLayout"] as DataTemplate;
layoutModeButton.IconUri = _gridButtonUri;
layoutModeButton.Text = "grid";
}
else
{
MainLongListSelector.LayoutMode = LongListSelectorLayoutMode.Grid;
MainLongListSelector.ItemTemplate = this.Resources["GridListLayout"] as DataTemplate;
layoutModeButton.IconUri = _listButtonUri;
layoutModeButton.Text = "list";
}
}
You might have figured out the answer already, but just to add to the conversation: this gives me really good performance for a fairly large amount of data. Maybe you can do something similar when navigating back to the page after changing the layout in settings?
Here is one walk around. (Maybe the problem will be corrected with WP 8.1 Update, along with others I've spotted working with LLS. I know - this idea is ugly, hard and so on, but maybe it will be enough for your purpose:
Becouse the problem concerns 'reloading' LLS, I've forced it to initialize it every time I navigate to the page (in fact I need to initialize the whole Page - it won't work only with LLS). I've moved InitializeComponent() and buttons events and so on to OnNavigatedTo():
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this._contentLoaded = false;
InitializeComponent();
first.Click += first_Click;
second.Click += second_Click;
ItemLongListSelector.ItemsSource = Items;
}
At least OnContentChanged() is fired that many Times its needed. Code you can find here.

Binding resp. DataTemplate confusion

I am still very new and trying my first serious data binding. I have read a lot about how it works, am just struggling with this concrete example. I have tried to read all links I could find on this, but most sources tend to be a bit imprecise at key spots. So here goes:
-My Application generates dynamically a variable 'PlayerList' of type 'List', where 'Player' is a complex object.
-I want to display this in a ListBox via Binding. Obvoiusly, since Player is a complex Object I want to create a DataTemplate for it. So I have something like this in the 'Window1.xaml':
<ListBox
Name="ListBox_Players"
ItemsSource="{Binding Source={StaticResource PlayerListResource}}"
ItemTemplate="{StaticResource PlayerTemplate}">
</ListBox>
and something like this in the 'App.xaml':
<DataTemplate x:Key="PlayerTemplate"> <!-- DataType="{x:Type Player}" -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=name}"/>
<TextBlock Text=", "/>
<TextBlock Text="{Binding Path=nrOfTabls}"/>
</StackPanel>
</DataTemplate>
Of course, this template will become more verbose later. So as you see above, I have tried to create a resource for the PlayerList variable, but have not managed yet, i.e., smthn. like this
<src: XXX x:Key="PlayerListResource"/>
where for XXX as I understand it I should enter the class of the Resource variable. I tried
List<Player>, List<src:Player>
etc., but obv. XAML has trouble with the '<,>' characters.
I also have another problem: By not declaring a resource but by direct binding (i.e., in C# writing "ListBox_Players.ItemsSource=PlayerList;") and deleting the 'ItemTemplate' declaration and overwriting the ToString() method of the Player class to output the name of the Player I have managed to see that the binding works (i.e., I get a list of Player names in the ListBox). But then, if I insert the template again, it displays only ','my Template does not work!
The fact that you're getting just commas without anything else suggests to me that either the names of Player members do not match the names in Path= in the DataTemplate (I had this problem at one point), or the relevant Player members are inaccessible.
I just tested what you've shown of your code so far, and it seemed to work fine. The only change I made was change this line:
ItemsSource="{Binding Source={StaticResource PlayerListResource}}"
to this line:
ItemsSource = "{Binding}"
This tells the program that it'll get the ItemsSource at run time.
My Player class was:
class Player {
public string name { get; set; }
public int nrOfTabls { get; set; }
}
and my MainWindow.xaml.cs was:
public partial class MainWindow : Window {
private ObservableCollection<Player> players_;
public MainWindow() {
InitializeComponent();
players_ =new ObservableCollection<Player> () {
new Player() {
name = "Alex",
nrOfTabls = 1,
},
new Player() {
name = "Brett",
nrOfTabls = 2,
},
new Player() {
name="Cindy",
nrOfTabls = 231,
}
};
ListBox_Players.ItemsSource = players_;
}
}

Categories