Merge pull request #222 from zadjii-msft/llama/icon-control

Fixes #213
Replaces PR #218

FYI @Ryken100 (thanks for the info and assist in debugging the issue and discussing possible avenues of resolution)
Thanks @zadjii-msft for validating the end path in #218

Before:
```xml
                        <Border x:Name="IconBorder"
                                Grid.Column="0"
                                Width="16"
                                Height="16"
                                Margin="0,0,0,0">
                            <!-- LoadIconBehavior will magically fill this border up with an icon -->
                            <Interactivity:Interaction.Behaviors>
                                <cmdpalUI:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
                            </Interactivity:Interaction.Behaviors>
                        </Border>
```

After:
```xml
                        <cpcontrols:IconBox
                            Grid.Column="0"
                            Width="16"
                            Height="16"
                            Margin="0,0,0,0"
                            SourceKey="{x:Bind Icon, Mode=OneWay}"
                            SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
```

The IconCacheProvider is the translation layer between having a light-weight control and our specific app's logic/desire for an icon cache, using the deferred event pattern:

```cs
public static partial class IconCacheProvider
{
    private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());

    public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
    {
        if (args.Key == null)
        {
            return;
        }

        if (args.Key is IconDataType iconData)
        {
            var deferral = args.GetDeferral();

            args.Value = await IconService.GetIconSource(iconData);

            deferral.Complete();
        }
    }
}
```

## Details

`IconBox` is a custom control that's a ContentControl, its generic (toolkitable) and should be able to be styled and templated (haven't tested, but no reason it shouldn't as a XAML `ContentControl`, should help @niels9001 a ton). 

It knows how to take an `IconSource` and create the underlying `IconElement` as its content.

It can also take any general value as a `SourceKey` and via an implementation of the `SourceRequested` event, translate a bound general object into the `IconSource` required. This is how caching can be provided by an application as well, for instance (like we'll do here). This uses the deferred events pattern to await the call to the `SourceRequested` event which may need to load data asynchronously

We create a static x:Bind helper `IconCacheProvider` to encapsulate our shared logic for our eventual Icon cache.

Also:
- Renamed IconCacheService.xaml.cs -> IconCacheService.cs 
- Removed old broken behavior (believe ultimate issue was due to instability in loaded/unloaded events, i.e. issue https://github.com/microsoft/microsoft-ui-xaml/issues/1900)
- XAML Styler also did its thing... (some conflict here in config from PowerToys to resolve later, imagine this is also a consequence of us not having CI setup in fork...)
This commit is contained in:
Mike Griese
2024-12-13 07:08:59 -06:00
committed by GitHub
8 changed files with 266 additions and 182 deletions

View File

@@ -3,14 +3,14 @@
x:Class="Microsoft.CmdPal.UI.Controls.ActionBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
Background="Transparent"
mc:Ignorable="d">
@@ -30,16 +30,13 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border x:Name="IconBorder"
Grid.Column="0"
Width="16"
Height="16"
Margin="0,0,0,0">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
Grid.Column="0"
Width="16"
Height="16"
Margin="0,0,0,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock Grid.Column="1" Text="{x:Bind Title}" />
</Grid>
@@ -55,18 +52,16 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border x:Name="IconBorder"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Width="20"
Height="20"
Margin="12,0,0,0"
CornerRadius="{StaticResource ControlCornerRadius}">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
x:Name="IconBorder"
Width="20"
Height="20"
Margin="12,0,0,0"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
CornerRadius="{StaticResource ControlCornerRadius}"
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
Grid.Column="1"
@@ -81,21 +76,19 @@
Spacing="6">
<Button
x:Name="PrimaryButton"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Height="40"
Padding="8,4,8,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Visibility="{x:Bind ViewModel.PrimaryAction.Name, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- <FontIcon Glyph="&#xEA3A;" /> -->
<Border Width="16"
Height="16"
Margin="4,4,4,4">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.PrimaryAction.Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
Width="16"
Height="16"
Margin="4,4,4,4"
SourceKey="{x:Bind ViewModel.PrimaryAction.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<StackPanel Orientation="Vertical" Spacing="2">
<TextBlock
@@ -113,21 +106,19 @@
</Button>
<Button
x:Name="SecondaryButton"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Height="40"
Padding="8,4,8,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- <FontIcon Glyph="&#xEA3A;" /> -->
<Border Width="16"
Height="16"
Margin="4,4,4,4">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.SecondaryAction.Icon, Mode=OneWay}" />
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
Width="16"
Height="16"
Margin="4,4,4,4"
SourceKey="{x:Bind ViewModel.SecondaryAction.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<StackPanel Orientation="Vertical" Spacing="1">
<TextBlock

View File

@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Common.Deferred;
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Deferred;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Controls;
/// <summary>
/// A helper control which takes an <see cref="IconSource"/> and creates the corresponding <see cref="IconElement"/>.
/// </summary>
public partial class IconBox : ContentControl
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
/// <summary>
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
/// </summary>
public IconSource? Source
{
get => (IconSource?)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
// Using a DependencyProperty as the backing store for Source. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(IconSource), typeof(IconBox), new PropertyMetadata(null, OnSourcePropertyChanged));
/// <summary>
/// Gets or sets a value to use as the <see cref="SourceKey"/> to retrieve an <see cref="IconSource"/> to set as the <see cref="Source"/>.
/// </summary>
public object? SourceKey
{
get => (object?)GetValue(SourceKeyProperty);
set => SetValue(SourceKeyProperty, value);
}
// Using a DependencyProperty as the backing store for SourceKey. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SourceKeyProperty =
DependencyProperty.Register(nameof(SourceKey), typeof(object), typeof(IconBox), new PropertyMetadata(null, OnSourceKeyPropertyChanged));
/// <summary>
/// Gets or sets the <see cref="SourceRequested"/> event handler to provide the value of the <see cref="IconSource"/> for the <see cref="Source"/> property from the provided <see cref="SourceKey"/>.
/// </summary>
public event TypedEventHandler<IconBox, SourceRequestedEventArgs>? SourceRequested;
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is IconBox @this)
{
switch (e.NewValue)
{
case null:
@this.Content = null;
break;
case FontIconSource fontIco:
fontIco.FontSize = @this.Width;
// For inexplicable reasons, FontIconSource.CreateIconElement
// doesn't work, so do it ourselves
// TODO: File platform bug?
IconSourceElement elem = new()
{
IconSource = fontIco,
};
@this.Content = elem;
break;
case IconSource source:
@this.Content = source.CreateIconElement();
break;
default:
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
}
}
}
private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is IconBox @this)
{
if (e.NewValue == null)
{
@this.Source = null;
}
else
{
_ = @this._queue.EnqueueAsync(async () =>
{
var eventArgs = new SourceRequestedEventArgs(e.NewValue);
if (@this.SourceRequested != null)
{
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
if (eventArgs.Value != null)
{
@this.Source = eventArgs.Value;
}
}
});
}
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Common.Deferred;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Controls;
/// <summary>
/// See <see cref="IconBox.SourceRequested"/> event.
/// </summary>
public class SourceRequestedEventArgs(object? key) : DeferredEventArgs
{
public object? Key { get; private set; } = key;
public IconSource? Value { get; set; }
}

View File

@@ -3,11 +3,11 @@
x:Class="Microsoft.CmdPal.UI.ListPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Interactions="using:Microsoft.Xaml.Interactions.Core"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
@@ -33,25 +33,23 @@
Spacing="8" />
<DataTemplate x:Key="TagTemplate" x:DataType="viewmodels:TagViewModel">
<!-- TODO: Actually colorize the tags again -->
<!-- TODO: Actually colorize the tags again -->
<StackPanel
Padding="4,2,4,2"
VerticalAlignment="Center"
BorderBrush="{ThemeResource TextBoxBorderThemeBrush}"
BorderThickness="1"
Orientation="Horizontal"
CornerRadius="4">
<Border x:Name="IconBorder"
Width="12"
Height="12"
Margin="0,0,4,0"
Visibility="{x:Bind HasIcon, Mode=OneWay}">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
CornerRadius="4"
Orientation="Horizontal">
<cpcontrols:IconBox
x:Name="IconBorder"
Width="12"
Height="12"
Margin="0,0,4,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind HasIcon, Mode=OneWay}" />
<TextBlock
VerticalAlignment="Center"
FontSize="12"
@@ -72,16 +70,14 @@
</Grid.ColumnDefinitions>
<Border x:Name="IconBorder"
Grid.Column="0"
Width="20"
Height="20"
Margin="4,0,4,0">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
x:Name="IconBorder"
Grid.Column="0"
Width="20"
Height="20"
Margin="4,0,4,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<StackPanel
Grid.Column="1"
@@ -105,12 +101,13 @@
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
</StackPanel>
<ItemsRepeater ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags}"
Grid.Column="2"
Visibility="{x:Bind HasTags}"
Layout="{StaticResource HorizontalStackLayout}" />
<ItemsRepeater
Grid.Column="2"
ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags}"
Layout="{StaticResource HorizontalStackLayout}"
Visibility="{x:Bind HasTags}" />
</Grid>
</ListViewItem>
</DataTemplate>

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.ExtViews;
namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
/// </summary>
public static partial class IconCacheProvider
{
private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
#pragma warning disable IDE0060 // Remove unused parameter
public static async void SourceRequested(IconBox sender, SourceRequestedEventArgs args)
#pragma warning restore IDE0060 // Remove unused parameter
{
if (args.Key == null)
{
return;
}
if (args.Key is IconDataType iconData)
{
var deferral = args.GetDeferral();
args.Value = await IconService.GetIconSource(iconData);
deferral.Complete();
}
}
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.UI.ExtViews;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;
namespace Microsoft.CmdPal.UI;
public partial class LoadIconBehavior : DependencyObject, IBehavior
{
private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
public IconDataType? Source
{
get => (IconDataType?)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
// Using a DependencyProperty as the backing store for Source. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(IconDataType), typeof(LoadIconBehavior), new PropertyMetadata(null, OnSourcePropertyChanged));
public DependencyObject? AssociatedObject { get; private set; }
public void Attach(DependencyObject associatedObject) => AssociatedObject = associatedObject;
public void Detach() => AssociatedObject = null;
private static async void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is LoadIconBehavior @this
&& @this.AssociatedObject is Border border)
{
var icoSource = await IconService.GetIconSource(@this.Source ?? new(string.Empty));
/* This causes a catastrophic failure...
if (border.Child != null)
{
VisualTreeHelper.DisconnectChildrenRecursive(border.Child);
border.Child = null;
}*/
if (icoSource is FontIconSource fontIco)
{
fontIco.FontSize = border.Width;
// For inexplicable reasons, FontIconSource.CreateIconElement
// doesn't work, so do it ourselves
IconSourceElement elem = new()
{
IconSource = fontIco,
};
border.Child = elem;
}
else
{
var icoElement = icoSource?.CreateIconElement();
border.Child = icoElement;
}
}
}
}

View File

@@ -3,13 +3,12 @@
x:Class="Microsoft.CmdPal.UI.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="Transparent"
mc:Ignorable="d">
@@ -32,7 +31,7 @@
<VisualState x:Name="DetailsVisible">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.IsDetailsVisible, Mode=OneWay}"/>
<StateTrigger IsActive="{x:Bind ViewModel.IsDetailsVisible, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DetailsContent.Visibility" Value="Visible" />
@@ -49,13 +48,14 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<controls:SearchBar
<cpcontrols:SearchBar
x:Name="SearchBox"
Grid.Row="0"
VerticalAlignment="Top"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
<Grid x:Name="ContentGrid"
<Grid
x:Name="ContentGrid"
Grid.Row="1"
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
@@ -66,18 +66,19 @@
<ColumnDefinition x:Name="DetailsColumn" Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Name="RootFrame"
Grid.Column="0"
IsNavigationStackEnabled="True" />
<Frame
Name="RootFrame"
Grid.Column="0"
IsNavigationStackEnabled="True" />
<Grid
<Grid
x:Name="DetailsContent"
Grid.Column="1"
Visibility="Collapsed"
Grid.Column="1"
Padding="8"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1,0,0,0">
BorderThickness="1,0,0,0"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -85,40 +86,37 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border x:Name="HeroImageBorder"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
Width="64"
Height="64"
Visibility="{x:Bind ViewModel.Details.HasHeroImage, Mode=OneWay}" >
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}" />
</Interactivity:Interaction.Behaviors>
</Border>
<cpcontrols:IconBox
x:Name="HeroImageBorder"
Width="64"
Height="64"
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind ViewModel.Details.HasHeroImage, Mode=OneWay}" />
<TextBlock
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
FontSize="20"
TextWrapping="WrapWholeWords"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.Details.Title, Mode=OneWay}"
Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}"/>
Text="{x:Bind ViewModel.Details.Title, Mode=OneWay}"
TextWrapping="WrapWholeWords"
Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />
<ScrollViewer Grid.Row="2" HorizontalAlignment="Stretch">
<labs:MarkdownTextBlock
<labs:MarkdownTextBlock
x:Name="DetailsMarkdown"
Text="{x:Bind ViewModel.Details.Body, Mode=OneWay}"
Background="Transparent" >
</labs:MarkdownTextBlock>
Background="Transparent"
Text="{x:Bind ViewModel.Details.Body, Mode=OneWay}" />
</ScrollViewer>
</Grid> <!--/DetailsContent-->
</Grid>
<!-- /DetailsContent -->
</Grid>
<controls:ActionBar
<cpcontrols:ActionBar
Grid.Row="2"
VerticalAlignment="Top"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />