Repositorio
https://github.com/GermanKuber/ReadApp
Clonamos el repositorio con el proyecto base.
git clone https://github.com/GermanKuber/ReadApp.git
Edito el archivo MainPage.xaml
<Grid Background="#FFFFFF"> <Grid.ColumnDefinitions> <ColumnDefinition Width="320"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid Grid.Column="0"> <Grid Background="#FF1D3F58"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="0*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="50"/> <RowDefinition/> </Grid.RowDefinitions> <ListView x:Name="listView" Grid.Row="1" Background="#FF1D3F58" d:LayoutOverrides="LeftMargin, RightMargin, TopMargin, BottomMargin"> </ListView> <TextBox x:Name="textBox" Margin="10,10,1,8" TextWrapping="Wrap" d:LayoutOverrides="Height, LeftMargin, RightMargin, TopMargin, BottomMargin" PlaceholderText="Buscar ..." Background="#FF1D3F58" Foreground="White" SelectionHighlightColor="White" > </TextBox> </Grid> </Grid> <Grid Grid.Column="1" Margin="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="13*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <Grid Grid.Row="0" Grid.Column="0" BorderThickness="0,0,0,2" > <Grid.ColumnDefinitions> <ColumnDefinition Width="200"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Ellipse Grid.Column="0" x:Name="Imangen" Margin="20 20 20 20" Stroke="Black" HorizontalAlignment="Right" Width="150" Height="150" > </Ellipse> <StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0 30 0 0" > <TextBlock Text="Descripción" TextWrapping="Wrap" ></TextBlock> <TextBlock TextWrapping="Wrap" ></TextBlock> </StackPanel> </Grid > <ScrollViewer x:Name="scrollViewer" Grid.Row="1" Grid.ColumnSpan="2" Margin="0,-0.333,0,0" Background="#B29E9494" > </ScrollViewer> <Grid Grid.RowSpan="2"/> </Grid> </Grid>
Tras agregar este código en el archivo MainPage.xaml, podemos presionar sobre ejecutar (o F5) y vamos a observar una ventana como esta:
03 – Bindings
En el archivo MainPage.xaml agrego
<Page.DataContext> <viewModels:MainPageDataViewModel /> </Page.DataContext>
Ahora vamos a agregar en nuestra clase MainPageDataViewModel (la cual contendrá toda la lógica de nuestra aplicación)
//TODO : 04 - Implemento la interface INotifyPropertyChanged public class MainPageDataViewModel : INotifyPropertyChanged { //implemento la propiead de la interface. public event PropertyChangedEventHandler PropertyChanged; public MainPageDateService MainPageDateService = new MainPageDateService(); private ReadRepository _readRepository = new ReadRepository(); public ConfigurationsViewModel Configuration = new ConfigurationsViewModel(); #region Public Properties //TODO : 03 - Agrego una lista observable, para derectar los cambios en la view public ObservableCollection<CommunityModel> ReadModels { get; set; } = new ObservableCollection<CommunityModel>(); //TODO : 05 - Propiedad que se utilizara bindear contra el item seleccionado private CommunityModel _selectedRead; public CommunityModel SelectedRead { get { return _selectedRead; } set { _selectedRead = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedRead))); } } //TODO : 06 - Agrego propiedad para bindear contra el texto que el usuario filtra private string _filter; public string Filter { get { return _filter; } set { if (value == _filter) return; _filter = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filter))); FilterTextAsync(); } } #endregion #region Commands //private ICommand _exampleCommand; //public ICommand ExampleCommand //{ // get // { // if (_exampleCommand == null) // { // _exampleCommand = new CommandHandler(((obj) => // { // ... // })); // } // return _exampleCommand; // } // set { _exampleCommand = value; } //} #endregion #region Private Properties #endregion #region Constructor public MainPageDataViewModel() { GenerateDummyData(); } #endregion #region Private Methods //TODO : 07 - Agrego metodo para filtrar y cargar los modelos a la lista private async Task FilterTextAsync() { if (_filter == null) _filter = ""; //Se filtran los readModels por Name var result = await _readRepository.GetByNameAsync(this.Filter); ReadModels.Clear(); result.ForEach(x => { ReadModels.Add(x); }); if (ReadModels.Count > 0) this.SelectedRead = ReadModels.First(); } #endregion private void GenerateDummyData() { var list = _readRepository.DataForView(); ReadModels.Clear(); list.ForEach(x => { ReadModels.Add(x); }); if (ReadModels != null && ReadModels.Count > 0) this.SelectedRead = ReadModels.First(); } }
Ahora remplazo el primer TextBock y el primer ListView con el siguiente código:
<ListView x:Name="listView" ItemTemplate="{StaticResource RememberModelTemplate}" ItemsSource="{Binding ReadModels}" Grid.Row="1" SelectedItem="{Binding SelectedRead, Mode=TwoWay}" Background="#FF1D3F58" d:LayoutOverrides="LeftMargin, RightMargin, TopMargin, BottomMargin"> </ListView> <TextBox x:Name="textBox" Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="10,10,1,8" TextWrapping="Wrap" d:LayoutOverrides="Height, LeftMargin, RightMargin, TopMargin, BottomMargin" PlaceholderText="Buscar ..." Background="#FF1D3F58" Foreground="White" SelectionHighlightColor="White" > </TextBox>
Remplazo el Ellipse y el StackPanel
<Ellipse Grid.Column="0" x:Name="Imangen" Margin="20 20 20 20" Stroke="Black" d:LayoutOverrides="TopPosition, BottomPosition" HorizontalAlignment="Right" Width="150" Height="150" > <Ellipse.Fill> <ImageBrush Stretch="Fill" ImageSource="{Binding SelectedRead.Picture}"/> </Ellipse.Fill> </Ellipse> <StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0 30 0 0" > <TextBlock Text="Descripción" TextWrapping="Wrap" ></TextBlock> <TextBlock Text="{Binding SelectedRead.Description}" TextWrapping="Wrap" ></TextBlock> </StackPanel>
Por ultimo remplazo el ultimo ScrollViewer por
<ScrollViewer x:Name="scrollViewer" Grid.Row="1" Grid.ColumnSpan="2" Margin="0,-0.333,0,0" d:LayoutOverrides="TopPosition, BottomPosition"> <ItemsControl ItemTemplate="{StaticResource ReadEventsTemplate}" ItemsSource="{Binding SelectedRead.Events}"> </ItemsControl> </ScrollViewer>
A continuación deberíamos de ver lo siguiente:
04 – Other Features
Reemplazo los dos AppBarButton con el siguiente código
<AppBarButton Command="{Binding DataContext.SendEmailCommand, ElementName=GridContext}" CommandParameter="{Binding}" x:Name="btnEmail" Content="Button" Icon="Mail" Height="54" HorizontalAlignment="Left" Margin="350,49,0,0" Label="Email" FontSize="19" d:LayoutOverrides="VerticalAlignment" Visibility="{Binding EmailVisibility, Mode=OneWay}"/> <AppBarButton Command="{Binding DataContext.AddNoticeToCommand, ElementName=GridContext}" CommandParameter="{Binding}" x:Name="btnCalendar" Content="Button" Icon="Calendar" Height="50" HorizontalAlignment="Left" d:LayoutOverrides="VerticalAlignment" Margin="430,50,0,0" />
Agrego los comandos correspondientes al MainPageDataViewModel
private ICommand _addNoticeToCommand; public ICommand AddNoticeToCommand { get { if (_addNoticeToCommand == null) { _addNoticeToCommand = new CommandHandler(((obj) => { MainPageDateService.AddNoticeToCalendarAsync((EventModel)obj); })); } return _addNoticeToCommand; } set { _addNoticeToCommand = value; } } private ICommand _sendEmailCommand; public ICommand SendEmailCommand { get { if (_sendEmailCommand == null) { _sendEmailCommand = new CommandHandler((async (obj) => { await MainPageDateService.SendEmailAsync((EventModel)obj, SelectedRead.Email); })); } return _sendEmailCommand; } set { _sendEmailCommand = value; } }
Modifico el constructor de MainPageDataViewModel
public MainPageDataViewModel() { if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) GenerateDummyData(); else FilterTextAsync(); this.Configurations = new ConfigurationsViewModel(); }
Agrego los comandos para configuración y navegación en MainPage.xaml
<Page.BottomAppBar> <CommandBar x:Name="commandBar" Margin="0,17,0,0" RenderTransformOrigin="0.5,0.5" > <CommandBar.SecondaryCommands> <AppBarToggleButton x:Name="appBarToggleButton" Label="Notificaciones" IsChecked="{Binding Configurations.NotificationsEnabled, Mode=TwoWay}" DataContext="{Binding}"/> <AppBarToggleButton x:Name="appBarToggleButton1" Label="Live" IsChecked="{Binding Configurations.LiveTileEnabled, Mode=TwoWay}" DataContext="{Binding}"/> <AppBarToggleButton x:Name="appBarToggleButton2" Label="Dark Theme" IsChecked="{Binding Configurations.DarkTheme, Mode=TwoWay}" DataContext="{Binding}"/> <AppBarSeparator/> <AppBarToggleButton x:Name="appBarButton" Label="About" Click="appBarButton_Click"/> </CommandBar.SecondaryCommands> <CommandBar.Content> <Grid/> </CommandBar.Content> </CommandBar> </Page.BottomAppBar>
Agrego el código para manejar el click de appBarButton en MainPage.xaml.cs
private void appBarButton_Click(object sender, RoutedEventArgs e) { //TODO : 07 - Boton de navegación Frame.Navigate(typeof(About)); }
Ahora vamos a registrar un servicio background que se encargue de generar las notificaciones y el live, para eso vamos al archivo App.xaml.cs y en el evento OnLaunched agregamos el siguiente código.
ReadAppBackgroundTask.Register();
Luego nos dirigimos al Package.appxmanifest
Y vamos a registrar nuestra clase para que sea el punto de entrada de esta tarea en background. Además debemos de dar los siguientes accesos.
Una vez que realizamos estos pasos, vamos a ejecutar nuestra aplicación. Con ella ejecutada vamos a ejecutar nuestra tarea en background de la siguiente forma.
Una vez mas nos dirigimos al App.xaml.cs y en el evento OnLaunched agregamos el siguiente código.
var vcdFile = await StorageFile.GetFileFromApplicationUriAsync( new Uri("ms-appx:///VoiceCommandDefinition.xml")); await VoiceCommandDefinitionManager .InstallCommandDefinitionsFromStorageFileAsync(vcdFile);
Este código registrara el comando de Cortana.
Ahora en vamos a sobre escribir el método OnActivated
protected override void OnActivated(IActivatedEventArgs args) { base.OnActivated(args); //TODO : 09 - Se configura para responder a la activación mediante voicecommand if (args.Kind == ActivationKind.VoiceCommand) { //The app was invoked via a voice command. } if (Window.Current.Content == null) { var rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; Window.Current.Content = rootFrame; rootFrame.Navigate(typeof(MainPage)); Window.Current.Activate(); } }
Y por ultimo vamos a registrar el servicio al igual que lo hicimos con nuestro background service
Ahora solo nos queda hablar con Cortana
05 – Behaviors
Vamos agregarle una pequeña transicion al momento de cargar a la lista de eventos para eso, nos dirigimos al MainPage.xaml, y vamos a editar el ScrolView que carga los eventos, y agregar un TransitionCollection
<ScrollViewer x:Name="scrollViewer" Grid.Row="1" Grid.ColumnSpan="2" Margin="0,-0.333,0,0" d:LayoutOverrides="TopPosition, BottomPosition"> <ItemsControl ItemTemplate="{StaticResource ReadEventsTemplate}" ItemsSource="{Binding SelectedRead.Events}"> <ItemsControl.ItemContainerTransitions> <TransitionCollection> <EntranceThemeTransition FromVerticalOffset="0" FromHorizontalOffset="200" IsStaggeringEnabled="True"/> </TransitionCollection> </ItemsControl.ItemContainerTransitions> </ItemsControl> </ScrollViewer>
Ahora vamos a agregar un VisualState, donde generemos un state, para el loading. Continuando en el mismo archivo vamos a pegar dentro del tag de nuestro Grid principal, el siguiente código:
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="LoadingState"> <VisualState x:Name="Loading"> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="stackPanel1" d:IsOptimized="True"/> </Storyboard> </VisualState> <VisualState x:Name="Loaded"> <Storyboard> <DoubleAnimation Duration="0:0:1" To="-550" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.GlobalOffsetY)" Storyboard.TargetName="textBlock1" d:IsOptimized="True"> <DoubleAnimation.EasingFunction> <BackEase Amplitude="0.2" EasingMode="EaseIn"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> <DoubleAnimation Duration="0:0:0.494" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="progressBar" d:IsOptimized="True"/> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(ProgressBar.IsIndeterminate)" Storyboard.TargetName="progressBar"> <DiscreteObjectKeyFrame KeyTime="0:0:0.494"> <DiscreteObjectKeyFrame.Value> <x:Boolean>False</x:Boolean> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0:0:1" To="0.4" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="textBlock1" d:IsOptimized="True"/> <DoubleAnimation Duration="0:0:1" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="grid" d:IsOptimized="True"/> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="grid"> <DiscreteObjectKeyFrame KeyTime="0:0:1"> <DiscreteObjectKeyFrame.Value> <Visibility>Collapsed</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Canvas.ZIndex)" Storyboard.TargetName="grid"> <DiscreteObjectKeyFrame KeyTime="0:0:1"> <DiscreteObjectKeyFrame.Value> <x:Int32>0</x:Int32> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0:0:1" To="0" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="commandBar" d:IsOptimized="True"> <DoubleAnimation.EasingFunction> <CircleEase EasingMode="EaseOut"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </VisualState> <VisualState x:Name="Error"> <VisualState.Setters> <Setter Target="textBlock1.(TextBlock.FontWeight)"> <Setter.Value> <FontWeight>Bold</FontWeight> </Setter.Value> </Setter> <Setter Target="textBlock1.(TextBlock.Text)" Value="Ocurrio un Error!!"/> </VisualState.Setters> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="stackPanel1" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="progressBar" d:IsOptimized="True"/> <ColorAnimation Duration="0" To="#FF931616" Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)" Storyboard.TargetName="textBlock1" d:IsOptimized="True"/> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="ScrenStates"> <VisualState x:Name="Big"> <VisualState.Setters> <Setter Target="splitView.(SplitView.IsPaneOpen)" Value="True"/> <Setter Target="splitView.(SplitView.DisplayMode)" Value="Inline"/> </VisualState.Setters> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="1150"/> </VisualState.StateTriggers> </VisualState> <VisualState x:Name="Small"> <VisualState.Setters> <Setter Target="splitView.(SplitView.DisplayMode)" Value="Overlay"/> </VisualState.Setters> <VisualState.StateTriggers> <AdaptiveTrigger MinWindowWidth="0"/> </VisualState.StateTriggers> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
Remplazamos todo el contenido de nuestro grid y a la altura del VisualStateManager
<SplitView x:Name="splitView" BorderThickness="0" Grid.ColumnSpan="2" DisplayMode="Inline" IsPaneOpen="{Binding OpenMenu, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> <SplitView.Pane> <Grid Grid.Column="0"> <Grid Background="#FF1D3F58"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="0*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="50"/> <RowDefinition/> </Grid.RowDefinitions> <ListView x:Name="listView" ItemTemplate="{StaticResource RememberModelTemplate}" ItemsSource="{Binding ReadModels}" Grid.Row="1" SelectedItem="{Binding SelectedRead, Mode=TwoWay}" Background="#FF1D3F58" d:LayoutOverrides="LeftMargin, RightMargin, TopMargin, BottomMargin"> </ListView> <TextBox x:Name="textBox" Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="10,10,1,8" TextWrapping="Wrap" d:LayoutOverrides="Height, LeftMargin, RightMargin, TopMargin, BottomMargin" PlaceholderText="Buscar ..." Background="#FF1D3F58" Foreground="White" SelectionHighlightColor="White"> </TextBox> </Grid> </Grid> </SplitView.Pane> <Grid Grid.Column="1" Margin="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="13*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <Grid Grid.Row="0" Grid.Column="0" BorderThickness="0,0,0,2" > <Grid.ColumnDefinitions> <ColumnDefinition Width="200"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Ellipse Grid.Column="0" x:Name="Imangen" Margin="20 20 20 20" Stroke="Black" d:LayoutOverrides="TopPosition, BottomPosition" HorizontalAlignment="Right" Width="150" Height="150" > <Ellipse.Fill> <ImageBrush Stretch="Fill" ImageSource="{Binding SelectedRead.Picture}"/> </Ellipse.Fill> </Ellipse> <StackPanel x:Name="stackPanel1" Grid.Column="1" VerticalAlignment="Center" Margin="0 30 0 0" > <TextBlock Text="Descripción" TextWrapping="Wrap" ></TextBlock> <TextBlock Text="{Binding SelectedRead.Description}" TextWrapping="Wrap" ></TextBlock> </StackPanel> </Grid > <ScrollViewer x:Name="scrollViewer" Grid.Row="1" Grid.ColumnSpan="2" Margin="0,-0.333,0,0" d:LayoutOverrides="TopPosition, BottomPosition"> <ItemsControl ItemTemplate="{StaticResource ReadEventsTemplate}" ItemsSource="{Binding SelectedRead.Events}"> <ItemsControl.ItemContainerTransitions> <TransitionCollection> <EntranceThemeTransition FromVerticalOffset="0" FromHorizontalOffset="200" IsStaggeringEnabled="True"/> </TransitionCollection> </ItemsControl.ItemContainerTransitions> </ItemsControl> </ScrollViewer> <Grid Grid.RowSpan="2"/> </Grid> </SplitView>
También vamos a descomentar la siguiente linea de código
<AppBarButton x:Name="appBarButton1" HorizontalAlignment="Stretch" Icon="AlignCenter" Label="Menu" VerticalAlignment="Stretch" d:LayoutOverrides="Height" Command="{Binding SwitchMenuCommand}" ></AppBarButton>
Descomentamos todo el código que tengo en el MainPage.xaml.cs, el cual se encargara de administrar el cambio de states
Volviendo al archivo MainPageDataViewModel.cs vamos a agregar el comando necesario para administrar la apertura y cierre del menú.
private bool _openMenu; public bool OpenMenu { get { return _openMenu; } set { _openMenu = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenMenu))); } } private ICommand _switchMenuCommand; public ICommand SwitchMenuCommand { get { if (_switchMenuCommand == null) { _switchMenuCommand = new CommandHandler(((obj) => { this.OpenMenu = !this.OpenMenu; })); } return _switchMenuCommand; } set { _switchMenuCommand = value; } }
Ahora vamos a agregar la pantalla de Loading. Para eso tomamos el siguiente código y lo vamos a poner dentro del grid principal, como hermano del splitview.
<Grid x:Name="grid" d:LayoutOverrides="LeftPosition, RightPosition, TopPosition, BottomPosition" Grid.ColumnSpan="2" Background="#FF2E2E2E" Opacity="0.9" Canvas.ZIndex="1" d:IsHidden="True"> <StackPanel x:Name="stackPanel1" HorizontalAlignment="Center" Margin="0" VerticalAlignment="Center"> <TextBlock x:Name="textBlock1" Style="{StaticResource HeaderTextBlockStyle}" TextWrapping="Wrap" Text="Cargando ..."> <TextBlock.Projection> <PlaneProjection/> </TextBlock.Projection> </TextBlock> <ProgressBar x:Name="progressBar" Height="10" VerticalAlignment="Stretch" IsIndeterminate="True" Margin="0,8,0,0" FontSize="22"/> </StackPanel> </Grid>
Generar Assets
Descargo el Assets Generator
https://marketplace.visualstudio.com/items?itemName=PeterR.UWPVisualAssetsGenerator
Editamos el package.appxmanifest
Slides