Just to get this out there - I am a complete newbie at XAML, Univeral Apps and ReactiveUI, so I'm probably missing something simple here.
To get a handle on ReactiveUI I thought I would try to port the ReactiveUI WPF Flickr sample from the documentation (Reactive UI 101) to a Windows Universal App. I am using Visual Studio 2015, ReactiveUI 6.5 with updated Splat.
I copied the code and made the necessary changes to make it compile for UWP. The current code does compile and open, but doesn't work properly.
I have two issues:
I have to click outside the Search: box to kick of the Flickr photo search. From what I understood I shouldn't have to click away, it should just pick up that I've typed something and kick on off the search after the timeout of 800ms. Debugging the photo search does work and a collection of objects is returned as my SearchResults.
The second issue is that even though I am getting a list of results, the ListBox is never populated. I've tried using the Binding as shown in the sample, and also using x:Bind as I learned how to do in all the UWP tutorials - no dice.
I downloaded another UWP sample (RxUI and UWP .Net Native Sample) and am looking into it's structure and setup to see what I missed but it's a bit more complicated to me right now.
If someone could point me in the right direction, it would help a lot.
Here's my XAML:
<Page
x:Class="ReactiveUIFlickerApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ReactiveUIFlickerApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:data="using:ReactiveUIFlickerApp.Models"
x:Name="MainPageView"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="PhotoDataTemplate"
x:DataType="data:FlickrPhoto">
<Grid MaxHeight="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="{x:Bind Url, Mode=OneWay}" Margin="6" MaxWidth="128"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Margin="6">
<TextBlock FontSize="14" FontWeight="Bold" Text="{x:Bind Title}" />
<TextBlock FontStyle="Italic" Text="{x:Bind Description}"
TextWrapping="Wrap" Margin="6" />
</StackPanel>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock FontSize="16"
FontWeight="Bold"
VerticalAlignment="Center">Search For:</TextBlock>
<TextBox Grid.Column="1"
Margin="6,0,0,0"
Text="{x:Bind PageViewModel.SearchTerm, Mode=TwoWay}"/>
<TextBlock Grid.Column="2"
Margin="6,0,0,0"
FontSize="16"
FontWeight="Bold"
Text="..."
Visibility="{x:Bind PageViewModel.SpinnerVisibility}" />
<ListBox Grid.ColumnSpan="3"
Grid.Row="1"
Margin="0,6,0,0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{x:Bind PageViewModel.SearchResults, Mode=OneWay}"
ItemTemplate="{StaticResource PhotoDataTemplate}" />
</Grid>
The XAML code-behind:
public sealed partial class MainPage : Page
{
public MainPageViewModel PageViewModel { get; private set;}
public MainPage()
{
PageViewModel = new MainPageViewModel();
this.InitializeComponent();
}
}
The MainPageViewModel (in its own View folder in the solution):
public class MainPageViewModel : ReactiveObject
{
string _SearchTerm;
public string SearchTerm
{
get { return _SearchTerm; }
set { this.RaiseAndSetIfChanged(ref _SearchTerm, value); }
}
public ReactiveCommand<List<FlickrPhoto>> ExecuteSearch { get; protected set; }
ObservableAsPropertyHelper<List<FlickrPhoto>> _SearchResults;
public List<FlickrPhoto> SearchResults => _SearchResults.Value;
ObservableAsPropertyHelper<Visibility> _SpinnerVisibility;
public Visibility SpinnerVisibility => _SpinnerVisibility.Value;
public MainPageViewModel()
{
ExecuteSearch =
ReactiveCommand.CreateAsyncTask(parameter => GetSearchResultsFromFlickr(this.SearchTerm));
this.WhenAnyValue(x => x.SearchTerm)
.Throttle(TimeSpan.FromMilliseconds(800), RxApp.MainThreadScheduler)
.Select(x => x?.Trim()) // null conditional operator
.DistinctUntilChanged()
.Where(x => !String.IsNullOrWhiteSpace(x))
.InvokeCommand(ExecuteSearch);
_SpinnerVisibility = ExecuteSearch.IsExecuting
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
.ToProperty(this, x => x.SpinnerVisibility, Visibility.Collapsed/*Hidden*/);
ExecuteSearch.ThrownExceptions.Subscribe(ex => { Debug.WriteLine(ex.ToString());/* Handle errors here */});
_SearchResults = ExecuteSearch.ToProperty(this, x => x.SearchResults, new List<FlickrPhoto>());
}
public static async Task<List<FlickrPhoto>> GetSearchResultsFromFlickr(string searchTerm)
{
var client = new HttpClient();
var stream = await client.GetStreamAsync(String.Format(CultureInfo.InvariantCulture,
"http://api.flickr.com/services/feeds/photos_public.gne?tags={0}&format=rss_200",
WebUtility.UrlEncode(searchTerm)));
//var doc = await Task.Run(() => XDocument.Load(String.Format(CultureInfo.InvariantCulture,
// "http://api.flickr.com/services/feeds/photos_public.gne?tags={0}&format=rss_200",
// WebUtility.UrlEncode(searchTerm))));
var doc = await Task.Run( () => XDocument.Load(stream) );
if (doc.Root == null)
return null;
var titles = doc.Root.Descendants("{http://search.yahoo.com/mrss/}title")
.Select(x => x.Value);
var tagRegex = new Regex("<[^>]+>", RegexOptions.IgnoreCase);
var descriptions = doc.Root.Descendants("{http://search.yahoo.com/mrss/}description")
.Select(x => tagRegex.Replace(WebUtility.HtmlDecode(x.Value), ""));
//.Select(x => tagRegex.Replace(HttpUtility.HtmlDecode(x.Value), ""));
var items = titles.Zip(descriptions,
(t, d) => new FlickrPhoto { Title = t, Description = d }).ToArray();
var urls = doc.Root.Descendants("{http://search.yahoo.com/mrss/}thumbnail")
.Select(x => x.Attributes("url").First().Value);
var ret = items.Zip(urls, (item, url) => { item.Url = url; return item; }).ToList();
return ret;
}
}