uwp界面自适应与数据绑定

资源

项目代码
项目演示视频

图片展示

功能和要求

界面设计

参考后面的截图,要求用到Grid(RowDefinitions、ColumnDefinitions)、 ScrollViewer、 StackPanel、BottomAppBar、RelativePannel、Image、TextBlock、DatePicker、APPBarButton。
自适应效果:当窗口宽度小于800时,只显示原本在界面左侧的列表部分,底部导航栏只显示Add图标;窗口宽度大于800时显示完整界面。窗口宽度小于600时,列表项中的图片不显示。(第二周验收只要求在界面宽度发生改变时,界面整体始终居中)
两块界面的右侧均需有滚动条。

页面的导航与跳转

点击MainPage底部的“+”按钮,跳转到NewPage;点击NewPage顶部的“←”按钮,跳转回MainPage。而在宽屏显示两块的情况下,点击“+”则无需跳转。窄屏状态下删除或添加后要跳回Mainpage界面。(第二周不要求宽屏情况)

页面内容

列表的每一项包括复选框、图片、文字(标题)。当复选框被勾选时有划掉的横线出现,取消勾选则横线消失。点击某一项,能跳转到详情页或者是宽屏状态下显示在右侧。详情页除了刚才的三个信息还有详情和日期,另有一个调节图片大小的滑块和两个按钮。

增删改

点击底部“+”或宽屏状态点击create新建,新建时检查Title、Description是否为空,DueDate是否正确(是否大于等于当前日期)。如果不正确,弹出对话框,显示错误信息。
在详情页点击update按钮可以修改信息。
新建中点击Cancel按钮,Title、Description置空,DueDate置为当前日期;修改中点击Cancel按钮,Title、Description、DueDate还原该清单项的详情数据。
选中一个清单项,点击底部删除图标可以删除该项。

主要解决问题

  • 项目的误区
  • 页面不同宽度自适应
  • ListView数据绑定
  • 不同页面跳转传值
  • 本地文件的图片选择以及绑定

项目的误区

确定newpage和mainpage的关系,在宽屏的时候不是newpage和mainpage合成了一个新的page,而是mainpage自身就是一个复杂的页面,既包含mainpage原本的内容,也要加入newpage那一部分,所以newpage和mainpage中的newpage那一部分不是同一个部分。

页面不同宽度自适应

这个本来可以在blend for visual studio中点击就可以完成,但是不推荐这种方法,一般来说blend for visual studio提供的功能没有代码多。第二因为涉及到listview所以与一般的自适应有些许不同,mainpage左边的部分是listView部分,是与右边分开的,如果VisualStateManager是在mainpage外面写的,则会造成,listView内部的图片查不到,明明已经写了但是是查不到的,可能有用UserControl解决的方法,但是我没有试出来。如果是在listView内部写VisualStateManager,那么又控制不到mainpage右边的部分,所以最好的方法是两个都写,一个在listView外面,一个在listView里面。

在ListView外面写入如下代码,当宽度大于800时,grid2也就是右边页面可见

<VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="VisualState800">
                    <VisualState.Setters>
                        <Setter Target="grid2.(UIElement.Visibility)" Value="Visible"/>
                    </VisualState.Setters>
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="800"/>
                    </VisualState.StateTriggers>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

在listView里面写入如下代码,当宽度在600和800之间,使得图片可见

<VisualStateManager.VisualStateGroups>
       <VisualStateGroup x:Name="VisualStateGroup">
              <VisualState x:Name="VisualState600">
                   <VisualState.Setters>
                         <Setter Target="Image.(UIElement.Visibility)" Value="Visible"/>
                    </VisualState.Setters>
                 <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="600"/>
                </VisualState.StateTriggers>
              </VisualState>
      </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

ListView数据绑定

文件结构

文件结构
首先要新建文件夹Models,在里面新建c#文件ListItem.cs,再新建文件夹ViewModels,在里面新建c#文件ListItemViewModels.cs。项目采用mvvm模式,ListItem.cs主要是申明数据结构,ListItemViewModels.cs是ListItem这个类实例的集合,用来删除,更新,新建操作。

ListItem.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Windows.UI.Xaml.Media.Imaging;

namespace first_project.Models
{
    public class ListItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public string id;

        public string title { get; set; }

        public string description { get; set; }

        public DateTimeOffset date { get; set; }

        public bool completed { get; set; }

        public BitmapImage src { get; set; }

        public bool Completed
        {
            get { return this.completed; }
            set
            {
                this.completed = value;
                NotifyPropertyChanged("Completed");
            }
        }
        public void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public ListItem()
        {

        }
        public ListItem(string title,string description ,DateTimeOffset date,BitmapImage input)
        {
            this.id = Guid.NewGuid().ToString();
            this.title = title;
            this.description = description;
            this.completed = false;
            this.date = date;
            this.src = input;
        }
    }
}

各个变量的作用以及继承类的作用

  • id是标识某个ListItem,用系统自带API生成
  • title,description,date不必多说,是某个ListItem的基本属性
  • completed这个属性,用到的地方是checkbox和Line绑定的时候
  • src是图片,主要是做本地图片选择的时候用到,如果不做这个功能可以不用
  • 有一个ListItem()构造函数,为了后面临时生成一个对象,所以没有处理
  • 继承NotifyPropertyChanged这个类,说明是向客户端发出某一属性值已更改的通知,具体是什么作用暂时还没懂,但是绑定checkbox和line时候会有用。

ListItemViewModels.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using first_project.Models;
using Windows.UI.Xaml.Media.Imaging;

namespace first_project.ViewModels
{
    public class ListItemViewModels
    {
        private ObservableCollection<Models.ListItem> allItems = new ObservableCollection<Models.ListItem>();

        public ObservableCollection<Models.ListItem> AllItems { get { return this.allItems; } }
        public void AddTodoItem(string title,string description,DateTimeOffset date,BitmapImage src)
        {
            this.allItems.Add(new Models.ListItem(title, description,date,src));
        }

        public void RemoveTodoItem(string id)
        {
            ListItem temp = new ListItem();
            for(int i = 0; i < allItems.Count(); i++)
            {
                if(allItems[i].id == id)
                {
                    temp = allItems[i];
                    break;
                }
            }
            this.allItems.Remove(temp);
        }

        public void UpdateTodoItem(string id,string title,string description,DateTimeOffset date,BitmapImage src)
        {
            /*ListItem temp = new ListItem();
            for (int i = 0; i < allItems.Count(); i++)
            {
                if (allItems[i].id == id)
                {
                    allItems[i].title = title;
                    allItems[i].description = description;
                    allItems[i].date = date;
                    allItems[i].src = src;
                    break;
                }
            }*/
            this.RemoveTodoItem(id);
            this.AddTodoItem(title, description, date, src);
        }
    }
}

关于ListItemViewModels的说明

  • 用了ObservableCollection方法
  • RemoveTodoItem中临时新建了一个ListItem对象,然后for循环查到删除之,不知道有没有更好的方法,直接通过id删除等,有removeAt函数,但是要求是int型的,而string id,是一串很长的码,不可能转成int型的。
  • UpdateTodoItem注释掉了一部分,如果不考虑图片的话,那部分是没问题的,但是加了图片之后,图片并没有更新,所以注释掉,先删除再创建。暂时没查明问题。

MainPage中ListView

<ListView IsItemClickEnabled="True" ItemClick="TodoTtem_ItemClicked" ItemsSource = "{x:Bind ViewModel.AllItems}" >
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="md:ListItem">
                        <UserControl>
                            <Grid x:Name="GridLeft">
                                <VisualStateManager.VisualStateGroups>
                                    <VisualStateGroup x:Name="VisualStateGroup">
                                        <VisualState x:Name="VisualState600">
                                            <VisualState.Setters>
                                                <Setter Target="Image.(UIElement.Visibility)" Value="Visible"/>
                                            </VisualState.Setters>
                                            <VisualState.StateTriggers>
                                                <AdaptiveTrigger MinWindowWidth="600"/>
                                            </VisualState.StateTriggers>
                                        </VisualState>
                                    </VisualStateGroup>
                                </VisualStateManager.VisualStateGroups>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="30" />
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <CheckBox Grid.Column="0" HorizontalAlignment="Center" x:Name="box"  Height="42" Width="81" Grid.ColumnSpan="2" Margin="0,4,11,4" IsChecked="{x:Bind Path=Completed, Converter={StaticResource CheckBoxConverter},Mode=TwoWay}"/>
                                <Image Grid.Column="1" x:Name="Image" HorizontalAlignment="Center" Source="{x:Bind src}" Width="50" Visibility="Collapsed" />
                                <TextBlock  Grid.Column="2" HorizontalAlignment="Left" Text="{x:Bind title}" VerticalAlignment="Center" Height="20"  Width="60" />
                                <Line x:Name="Line" Grid.Column="2" Stretch="Fill" Stroke="Black" StrokeThickness="2" Visibility="{x:Bind Path=Completed, Converter={StaticResource LineConverter},Mode=OneWay}" X1="1" Grid.ColumnSpan="2"  />
                            </Grid>
                        </UserControl>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>

关于ListView的说明

  • ItemClick=”TodoTtem_ItemClicked”这个函数,实现的功能是如果是宽度在800以上,则点击屏幕下方的加号,不会跳转新页面,直接将相关的信息显示在右侧,如果是小于宽度800,则跳转到NewPage界面并显示数据。
  • ItemsSource = “{x:Bind ViewModel.AllItems}” 这里的ViewModel经常会找不到,个人觉得是和MainPage.cs中是否在MainPage()构造函数中实例化有关ViewModel = new ListItemViewModels();
  • Checkbox的绑定,Checkbox本来是要和line绑定的,但是在ListView里面,和平时有不一样,无法绑定到Line,于是借助中间变量completed,将Checkbox IsChecked与Completed做绑定,用了双向绑定,IsChecked="{x:Bind Path=Completed, Converter={StaticResource CheckBoxConverter}, Mode=TwoWay},因为需要改变数据completed的值,当checkbox为勾选状态使得completed为正,虽然都是bool类型的值,但是不明白为什么不可以直接绑定,于是用转换器,使得可以绑定。
  • Line的绑定,Line也和Completed绑定,这个只要单向绑定即可,completed的值为真的时候Line的Visibility属性就为Visible,否则为Collapsed。这个地方做了很久都没有反应,最后试了ListItem继承INotifyPropertyChanged类,并补全相关代码,成功了。
  • Image和TextBlock直接绑定即可,无需转换,其实我对于Image还是有疑问的,因为Image的Source和src类型不匹配,src是BitmapImage类型,而它的地址是UriSource,所以不明白。

    converter.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml;

namespace first_project
{
    public class CheckBoxConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            bool temp = (bool)value;
            if(temp == true)
            {
                return true;
            }
            else
            {
                return false;
            }

        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            return (bool)value;
        }
    }

        public class LineConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, string language)
            {
                bool temp = (bool)value;
                if (temp)
                    return Visibility.Visible;
                return Visibility.Collapsed;
            }

            public object ConvertBack(object value, Type targetType, object parameter, string language)
            {
                throw new NotImplementedException();
            }
        }
}
  • 同样继承一个类IValueConverter,原因不知道
  • 剩下的应该不难看懂

    不同页面跳转传值

    应该是有用全局变量的方法,但这里用navigateTo的方法

contenet.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using first_project.ViewModels;
using first_project.Models;

namespace first_project
{
    public class Content
    {
        public string id { get; set; }
        public ListItemViewModels result { get; set; }
        public Content()
        {
            id = "";
        }
        public Content(string id,ListItemViewModels input)
        {
            this.id = id;
            this.result = input;
        }
    }
}

content.cs的作用

个人觉得有点冗余,但是也是因为没有找到好的解决方案,因为一开始考虑的是将数据全都传到第二个页面,如果第二个页面返回,肯定也要将所有数据再传回来,所以肯定不能只传一个ListItem,而要传allItems,但是只传allItems,就不知道点击的是哪一个,所以还要穿id,于是将两个整合成一个类,传递到第二个页面,应该有更好的方法。

点击ListItem传递的代码

public void TodoTtem_ItemClicked(object sender, ItemClickEventArgs e)
        {
            var temp = (ListItem)e.ClickedItem;
            currentId = temp.id;
            if (Window.Current.Bounds.Width > 800)
            {
                Create_To_Update();
                Delete.Visibility = Visibility.Visible;
                TitleBlock.Text = temp.title;
                DetailBlock.Text = temp.description;
                Date.Date = temp.date;
                NewImage.Source = temp.src;
            }
            else
            {
                Content result = new Content(currentId, ViewModel);
                this.Frame.Navigate(typeof(NewPage), result);
            }

实际上有作用的就`this.Frame.Navigate(typeof(NewPage), result);这一句,将result传到NewPage页面。

点击加号传递的代码

private void addBarClick(object sender, RoutedEventArgs e)
        {
            if(isClick == true)
            {
                Content result = new Content("NULL", ViewModel);
                this.Frame.Navigate(typeof(NewPage),result);
            }
        }

将id值设为NULL,为了接受时判读做准备

接受的代码

protected override void OnNavigatedTo(NavigationEventArgs e)
        { 
            base.OnNavigatedTo(e);
            temp = (Content)e.Parameter;
            if(temp.result.AllItems.Count() > 0 && temp.id != "NULL")
            {
                Create_To_Update();

                ListItem result = new ListItem();
                for (int i = 0; i < temp.result.AllItems.Count(); i++)
                {
                    if (temp.id == temp.result.AllItems[i].id)
                    {
                        result = temp.result.AllItems[i];
                        break;
                    }
                }
                TitleBlock.Text = result.title;
                DetailBlock.Text = result.description;
                Date.Date = result.date;
                NewImage.Source = result.src;

            }
            else
            {
                Update_To_Create();
            }
        }

首先判断是点页面下面加号跳转的,还是点击某个ListView跳转的,用的就是判断temp.id是否是NULL,依然是临时变量找实例。

页面返回时数据消失的解决

本来考虑的是用NewPage传值给MainPage,这就要在MainPage中写NavigateTo函数,但是实际上很难行得通,因为程序一开始运行就要进入MainPage页面,但那个时候没有任何值传给MainPage,所以就会报错,于是采用其他方法,用NavigationCache,即使传递了值,MainPage中任然会有值,代码也比较简单,只要在MainPage的构造函数中加上NavigationCacheMode = NavigationCacheMode.Enabled;就可以了。

本地文件的图片选择以及绑定

本地图片选择代码

private async void Select_Click(object sender, RoutedEventArgs e)
        {
            //文件选择器  
            FileOpenPicker openPicker = new FileOpenPicker();
            //选择视图模式  
            openPicker.ViewMode = PickerViewMode.Thumbnail;
            //openPicker.ViewMode = PickerViewMode.List;  
            //初始位置  
            openPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
            //添加文件类型  
            openPicker.FileTypeFilter.Add(".jpg");
            openPicker.FileTypeFilter.Add(".jpeg");
            openPicker.FileTypeFilter.Add(".png");

            StorageFile file = await openPicker.PickSingleFileAsync();

            if (file != null)
            {
                using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
                {
                    var srcImage = new BitmapImage();
                    await srcImage.SetSourceAsync(stream);
                    NewImage.Source = srcImage;
                }
            }
        }

这个网上基本上都能找到,问题在于最后一步NewImage.Source = srcImage; 我不是很清楚srcImage的类型。本来是没有图片的,后来加上这个功能,从头到尾都添加 了BitmapImage类型的值,一开始是添加string类型的值保存ImageSource的地址,发现类型不一样,于是放弃,采用BitmapImage,但是这里有个问题,就是BitmapImage和Image类型不一样,于是去官网找了一下,产生了新的idea,用BitmapImage标签,代替Image标签,然后发现不行,最后还是决定将Image类型转换成BitmapImage,BitmapImage result = NewImage.Source as BitmapImage; 就这一句代码,解决了所有问题,使得NewImage.Source可以直接被BitmapImage的值赋值,于是就解决了所有问题。

建议和提示

  • 很多找不到的东西,可能是没有加public,然后用点运算符的时候,系统没有显示里面的成员,误以为出了问题
  • 记得加using,因为这个问题,我一般都给了代码的头文件引用,否则很麻烦

猜你喜欢

转载自blog.csdn.net/yaoxh6/article/details/79745522