dotnet 职业技术学院

XAML

dotnet 职业技术学院

| xaml

按类别查找文章:XAML


uwp dotnet dotnet-core dotnet-standard csharp 技术 gif解析 wpf windows C# WPF D2D DirectX UWP DirectComposition win2d SharpDX VisualStudio dotnetremoting rpc visualstudio Win10 编程 slack web team msbuild linux xamarin ios resharper git nuget algorithm powershell ime vscode directx roslyn markdown ui ux miscellaneous cpp sharpdx algorithms JavaScript Unicode c# 工具 系统 架构 编译 数学 几何 C#入门 原理 Powershell 性能测试 命令行 asp aspdotnetcore dotnetcore 控制台 WMI .net framework .net源代码 源代码分析 Latex C 算法 Emit Roslyn MSBuild 编译器 调试 渲染 Resharper 打包 Pandoc PowerShell VisualStudio插件 TotalCommander 软件 Jenkins gif await 安装包 InnoSetup 触摸 WPF调试 图片处理 黑科技 UI sublimetext usb 笔迹 输入法 数据库 sqlite Framework dotnetframework remoting 布局 mvvm frp Avalonia 设计规范 规范 反射 jekyll DevOps AzureDevOps 源代码 多线程 VisualStudio调试 doevents 性能优化 水印 uwp文件 pip python 软件设计 文档 docfx 资源分享 p2p 爬虫 SublimeText AE dotpeek 反编译 btsync pandoc Telegram 聊天软件 微信 P2P PPT v8 .NET JVM Direct2D MobaXterm 代理 ssh vps 代理服务器 mock 单元测试 NuGet dnc 进程通信 IPC pipe ScrollViewer WPF源代码 ink dotnettool tool Github GithubAction Diagnostics DUMP Xamarin GTK
2020
03-05 2020

xaml 添加 region

本文告诉大家如何在 xaml 添加 region

03-05 2020

WPF 好看的矢量图标

本文告诉大家一个好用的网站,里面提供很多好看的图标。

03-05 2020

Roslyn 如何在 Target 引用 xaml 防止文件没有编译

在使用新的项目格式,可以使用 Target 添加项目,但是有一些项目需要在合适的时候添加,如果添加早了,那么会让用户看到这些文件,如果添加的时间是在引用编译之后,那么文件将无法进行编译。

2019
03-09 2019

WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑定了

如果你写了一个 MarkupExtension 在 XAML 当中使用,你会发现你在 MarkupExtension 中定时的属性是无法使用 XAML 绑定的,因为 MarkupExtension 不是一个 DependencyObject

本文将给出解决方案,让你能够在任意的类型中写出支持 XAML 绑定的属性;而不一定要依赖对象(DependencyObject)和依赖属性(DependencyProperty)。


问题

下面是一个很简单的 MarkupExtension,用户设置了什么值,就返回什么值。拿这么简单的类型只是为了避免额外引入复杂的理解难度。

public class WalterlvExtension : MarkupExtension
{
    private object _value;

    public object Value
    {
        get => _value;
        set => _value = value;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Value;
    }
}

可以在 XAML 中直接赋值:

<Button Content="{local:Walterlv Value=blog.walterlv.com" />

但不能绑定:

<TextBox x:Name="SourceTextBox" Text="blog.walterlv.com" />
<Button Content="{local:Walterlv Value={Binding Text, Source={x:Reference SourceTextBox}}}" />

因为运行时会报错,提示绑定必须被设置到依赖对象的依赖属性中。在设计器中也可以看到提示不能绑定。

运行时报错

设计器的警告

解决

实际上这个问题是能够解决的(不过也花了我一些时间思考解决方案)。

既然绑定需要一个依赖属性,那么我们就定义一个依赖属性。非依赖对象中不能定义依赖属性,于是我们定义附加属性。

// 注意:这一段代码实际上是无效的。

public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
    "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));

public object Value
{
    get => ???.GetValue(ValueProperty);
    set => ???.SetValue(ValueProperty, value);
}

这里问题来了,获取和设置附加属性是需要一个依赖对象的,那么我们哪里去找依赖对象呢?直接定义一个新的就好了。

于是我们定义一个新的依赖对象:

// 注意:这一段代码实际上是无效的。

public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
    "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));

public object Value
{
    get => _dependencyObject.GetValue(ValueProperty);
    set => _dependencyObject.SetValue(ValueProperty, value);
}

private readonly DependencyObject _dependencyObject = new DependencyObject();

现在虽然可以编译通过,但是我们会遇到两个问题:

  1. ValueProperty 的变更通知的回调函数中,我们只能找到 _dependencyObject 的实例,而无法找到外面的类型 WalterlvExtension 的实例;这几乎使得 Value 的变更通知完全失效。
  2. Valueset 方法中得到的 value 值是一个 Binding 对象,而不是正常依赖属性中得到的绑定的结果;这意味着我们无法直接使用 Value 的值。

为了解决这两个问题,我必须自己写一个代理的依赖对象,用于帮助做属性的变更通知,以及处理绑定产生的 Binding 对象。在正常的依赖对象和依赖属性中,这些本来都不需要我们自己来处理。

方案

于是我写了一个代理的依赖对象,我把它命名为 ClrBindingExchanger,意思是将 CLR 属性和依赖属性的绑定进行交换。

代码如下:

public class ClrBindingExchanger : DependencyObject
{
    private readonly object _owner;
    private readonly DependencyProperty _attachedProperty;
    private readonly Action<object, object> _valueChangeCallback;

    public ClrBindingExchanger(object owner, DependencyProperty attachedProperty,
        Action<object, object> valueChangeCallback = null)
    {
        _owner = owner;
        _attachedProperty = attachedProperty;
        _valueChangeCallback = valueChangeCallback;
    }

    public object GetValue()
    {
        return GetValue(_attachedProperty);
    }

    public void SetValue(object value)
    {
        if (value is Binding binding)
        {
            BindingOperations.SetBinding(this, _attachedProperty, binding);
        }
        else
        {
            SetValue(_attachedProperty, value);
        }
    }

    public static void ValueChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ClrBindingExchanger) d)._valueChangeCallback?.Invoke(e.OldValue, e.NewValue);
    }
}

这段代码的意思是这样的:

  1. 构造函数中的 owner 参数完全没有用,我只是拿来备用,你可以删掉。
  2. 构造函数中的 attachedProperty 参数是需要定义的附加属性。
    • 因为前面我们说过,有一个附加属性才可以编译通过,所以附加属性是一定要定义的
    • 既然一定要定义附加属性,那么就可以用起来,接下来会用
  3. 构造函数中的 valueChangeCallback 参数是为了指定变更通知的,因为前面我们说变更通知不好做,于是就这样代理做变更通知。
  4. GetValueSetValue 这两个方法是用来代替 DependencyObject 自带的 GetValueSetValue 的,目的是执行我们希望特别执行的方法。
  5. SetValue 中我们需要自己考虑绑定对象,如果发现是绑定,那么就真的进行一次绑定。
  6. ValueChangeCallback 是给附加属性用的,因为用我的这种方法定义附加属性时,只能写出相同的代码,所以干脆就提取出来。

而用法是这样的:

public class WalterlvExtension : MarkupExtension
{
    public WalterlvExtension()
    {
        _valueExchanger = new ClrBindingExchanger(this, ValueProperty, OnValueChanged);
    }

    private readonly ClrBindingExchanger _valueExchanger;

    public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
        "Value", typeof(object), typeof(WalterlvExtension),
        new PropertyMetadata(null, ClrBindingExchanger.ValueChangeCallback));

    public object Value
    {
        get => _valueExchanger.GetValue();
        set => _valueExchanger.SetValue(value);
    }

    private void OnValueChanged(object oldValue, object newValue)
    {
        // 在这里可以处理 Value 属性值改变的变更通知。
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Value;
    }
}

对于一个属性来说,代码确实多了些,这实在是让人难受。可是,这可以达成目的呀!

解释一下:

  1. 定义一个 _valueExchanger,就是在使用我们刚刚写的那个新类。
  2. 在构造函数中对 _valueExchanger 进行初始化,因为要传入 this 和一个实例方法 OnValueChanged,所以只能在构造函数中初始化。
  3. 定义一个附加属性(前面我们说了,一定要有依赖属性才可以编译通过哦)。
    • 注意属性的变更通知方法,需要固定写成 ClrBindingExchanger.ValueChangeCallback
  4. 定义普通的 CLR 属性 Value
    • GetValue 方法要换成我们自定义的 GetValue
    • SetValue 方法也要换成我们自定义的 SetValue 哦,这样绑定才可以生效
  5. OnValueChanged 就是我们实际的变更通知,这里得到的 oldValuenewValue 就是你期望的值,而不是我面前面奇怪的绑定实例。

于是,绑定就这么在一个普通的类型和一个普通的 CLR 属性中生效了,而且还获得了变更通知。

参考资料

本文没有任何参考资料,所有方法都是我(walterlv)的原创方法,因为真的找不到资料呀!不过在找资料的过程中发现了一些没解决的文档或帖子:

03-04 2019

使用 Xamarin 开发 iOS 键盘扩展(含网络访问)

作为一位 .NET 技术的死忠,开发 iOS 应用当然要使用 Xamarin 啦!

本文用我的阅读的文档和实践为素材,介绍如何使用 Xamarin 开发一个 iOS 的键盘扩展。


你可以在 Walterlv.CloudKeyboard 仓库中获得本文所述的全部源代码。

搭建环境

本文不会花篇幅来讲如何搭建 Xamarin iOS 开发的环境,不然这篇文章就没有重点。

于是,请阅读这一篇来了解如何搭建 Xamarin iOS 的开发环境:

  1. 安装调试工具:Mac 部分 Xamarin开发(Mac开发)环境搭建 - 简书
  2. 安装调试工具:Windows 部分 vs2017开发IOS(vs2017 xamarin 连接mac) - ManGo.XYZ - CSDN博客
  3. 申请开发者账号:https://developer.apple.com/register/阅读这里了解坑
  4. 准备一根 Type-C 到 Lightning 的数据线,用于 Mac 从 Mac 部署到真机进行调试

你需要了解的 iOS 键盘扩展的背景知识

了解以下背景知识,有助于我们接下来开发的时候少踩一些坑。当然我不会在这里说 iOS 应用开发的所有背景知识,只会说与 iOS 键盘扩展相关的部分。

  1. iOS 键盘扩展是 iOS 扩展的一种,而 iOS 扩展是 iOS 8.0 才开始引入的概念。
  2. iOS 扩展需要有一个 iOS 普通应用作为容器一起打包;所以,你需要创建两个项目来完成 iOS 键盘扩展的开发。
    • 在后文,我们将直接使用 iOS 容器应用来描述这个概念
    • 扩展的包标识符(Bundle Identifier)必须以容器应用的包标识符字符串作为开头
  3. iOS 扩展和 iOS 容器应用会被视为两款完全不同的应用,互相之前不能共享任何数据。
    • 如果真的要共享数据,就需要像其他两款不同应用共享数据一样的处理方式
  4. iOS 键盘扩展默认是不能访问网络的,你需要声明允许访问网络,并获得用户的同意才行。

创建 iOS 键盘扩展项目

第一步:创建 Xamarin.Forms 项目。

这个不用太在意里面的实现,因为它只是我们的“容器项目”(前面有介绍)。实际上在本文我们完全不会碰这个项目里面的代码,只是为了配置我们的 iOS 应用包而已。未来你可以在这个容器应用里面做键盘的个性化设置。

创建 Xamarin.Forms 项目

然后,选择 iOS 平台。

我们只需要 iOS 端。因为对于键盘,不同系统的实现差异很大,之间共享的代码只能是非键盘部分的代码了。

我们只选择 iOS 平台

第二步:创建 iOS 键盘扩展项目

创建新项目

创建 Custom Keyboard Extension 项目

创建完成之后的三个项目

当你创建完之后,你会看到三个不同的项目。

你可能发现 Walterlv.KeyboardExtension.Keyboard 项目有些奇怪,里面有 Main 函数和 AppDelegate,按道理这是一个主程序包。然而实际测试中单独有这个项目是跑不起来的(这可能是一个 Bug,如果修复了,请在下面评论或者邮件告知我,谢谢了)。

于是,Main 和 AppDelegate 这两个文件是可以删除的。如果你强迫症,就删掉吧。当然不删掉也不影响,不过我删掉了。

第三步:引用 iOS 键盘扩展项目

在 iOS 容器应用上面添加键盘扩展项目作为引用。

在 iOS 容器应用上添加引用

引用键盘扩展项目

如果你感兴趣去查看 Walterlv.KeyboardExtension.iOS 项目中对 Walterlv.KeyboardExtension.Keyboard 项目的引用节点的话,你会发现 Xamarin 已经自动为这个项目标记上了 <IsAppExtension />。只有加上了 AppExtension 标记,Xamarin 才会把这个项目作为 iOS 扩展项目进行打包。

<ProjectReference Include="..\..\Walterlv.KeyboardExtension.Keyboard\Walterlv.KeyboardExtension.Keyboard.csproj">
    <Project>{d6f006e7-3c98-4b97-b2d5-4d2e3bc2f945}</Project>
    <Name>Walterlv.KeyboardExtension.Keyboard</Name>
    <IsAppExtension>true</IsAppExtension>
    <IsWatchApp>false</IsWatchApp>
</ProjectReference>

在以上三个步骤完成之后,理论上你是可以正常编译此项目的。

可以编译通过

配置包信息

iOS 应用的包信息存储在 plist 中。所以在这一节,你需要正确配置两个项目的 plist。

没错!是两个项目。还记得前面背景知识里面我们说到容器项目和扩展项目就是两个不同的应用吗?

配置 plist 的方法,就是在 Visual Studio 里面双击这个文件。

按照下图这样配置:

配置两个项目的 plist 文件

说明:

  1. Application Name 对应 plist 中的 CFBundleDisplayName 属性,也就是应用的显示名称。
    • 对于容器应用,就是 iOS 图标下面的名称,对于键盘,就是切换键盘的时候所用的名称。
    • 下图中 iOS 应用图标下面的名称 CloudKeyboard 就是我在 Walterlv.CloudKeyboard 项目中的容器应用的名称。
    • 下图中在 iOS 切换键盘时,Cloud 就是我在 Walterlv.CloudKeyboard 项目中的键盘名称。
  2. 扩展项目的 Bundle Identifier 名称必须以容器项目的 Bundle Identifier 名称作为前缀。
    • 如果不满足要求,部署时扩展将不会生效。

iOS 应用图标

iOS 上切换键盘

至此,你的项目可以直接编译了。如果你有真机部署环境,都可以直接部署到真机上看效果了。

真机部署调试

本文不会花篇幅来讲如何真机部署调试,不然这篇文章就没有重点。

但是你可以阅读:使用 Xamarin 在 iOS 真机上部署应用进行调试

当然这是 Mac 版本的(毕竟我在 Windows 上实际也没有成功真机调试过,我是 git 同步到 Mac 上用 Visual Studio for Mac 来真机调试的)。

只是你需要注意做这些内容:

  1. 你需要同意一份开发者证书(不然打不开应用):
    • 设置 -> 通用 -> 设备管理 -> [自己的开发者账号] -> 信任
  2. 还需要打开这个键盘(不然看不到键盘):
    • 设置 -> 通用 -> 键盘 -> 添加新键盘… -> [选择我们刚刚开发的键盘]

下面是我部署到真机上之后,在亮暗两种不同的界面下的键盘截图(就是上面的项目,没有改任何代码):

键盘真机部署后的运行效果

处理键盘的文字输入、退格和确定

我们把 Walterlv.CloudKeyboard.iOS.Extension 也就是那个键盘扩展项目删除得只剩下 KeyboardViewController.cs 了,我们也只需要在这个类中写代码而已。

要控制文字输入,就是使用 TextDocumentProxy 实例。我们的 KeyboardViewController 继承自 UIInputViewController,于是我们能够在类中直接使用 TextDocumentProxy 实例。

在光标处插入文字:

TextDocumentProxy.InsertText("walterlv");

如果要插入换行或者确认输入,则使用:

TextDocumentProxy.InsertText("\n");

在光标处删除前一个字:

TextDocumentProxy.DeleteBackward();

如果想要清空文本,则可以循环删除:

while (TextDocumentProxy.HasText)
{
    TextDocumentProxy.DeleteBackward();
}

你没有办法删除后一个字,也不能获取到用户输入的任何内容。

关于换行,特别注意:如果文本框被设置为发送或者其他非换行的功能,那么使用 InsertText 单独插入换行时才能正常执行这些功能。如果调用此代码之前还有其他的插入文字,那么最终就只会是换行,而不会执行其他的功能。实际上我在这一点上踩了坑,导致在 QQ 或者其他工具中只能实现换行,而无法发送消息。

iOS 的键盘有不同种类的确认,需要键盘针对 TextDocumentProxy. 我还没有找到办法直接完成文本的输入,例如执行确认按钮的逻辑。而确认按钮有这么些不同的情况:

// 我当然是写 C# 语言版本的枚举,而不是 Object-C 版本的啦。
public enum UIReturnKeyType : long
{
    Default,
    Go,
    Google,
    Join,
    Next,
    Route,
    Search,
    Send,
    Yahoo,
    Done,
    EmergencyCall,
    Continue,
}

添加键盘的网络访问支持

允许完全访问(包括网络)

纯本地的键盘很难在打字速度上获得优势,各种主流的输入法也通常借助网络来提高自身的输入准确度。

用户需要在键盘设置里面开启键盘的“允许完全访问”才能让对应的输入法获得网络访问的权限。如果用户没有给权限,那么网络访问的时候键盘扩展就会出现异常,然后闪退。

允许完全访问

然而如果你去我们刚刚开发的输入法中看,你会发现我们的输入法没有提供这样的选项可以设置。那么如何能够添加这个设置以便进行网络访问呢?

方法是修改键盘扩展项目的 Info.plist 文件。这个时候的修改,我们就不能使用 Visual Studio 中自带的 plist 编辑器了,我们需要使用文本编辑器来编辑 plist 文件。

在你的 Info.plist 文件中找到 RequestsOpenAccess 属性,然后将它分值从 false 改为 true

    <key>RequestsOpenAccess</key>
--  <false/>
++  <true/>

这个属性设为 true 之后,再次部署,你将可以在你的键盘设置里面看到“允许完全访问”的设置项。开启之后,你就能在你的键盘里面访问网络了。

允许访问 http 不安全网络

一般来说你不用阅读这一小节的内容。因为现在基本上各种服务都已经是 https 了,http 基本已经绝迹。但是如果你需要临时部署一个服务,没来得及申请 https 证书的话,那么就需要使用本小结的内容让你的键盘支持 http 的访问。

继续打开你的键盘扩展项目的 Info.plist 文件,在根字典的最后添加一个完整的字典属性 NSAppTransportSecurity

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>walterlv.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

特别注意,里面的 walterlv.com 需要换成你自己的域名。是域名,不用包含端口号。

这样,你就能在键盘中访问 http://walterlv.com 了。

本文总结

  1. 本文介绍了使用 Xamarin 开发 iOS 键盘插件的背景知识。
    • 必须了解这些知识才不会在一些不太重要的坑上耗费太长时间。
  2. 本文教大家如何开发 iOS 键盘插件,主要是项目组织以及写代码。
    • 至少,使用文本编写出来的代码,能够在不作任何修改的情况下部署到真机。(实际上我们只在 KeyboardViewController.cs 中加了寥寥几行代码。)
  3. 本文不涉及到搭建开发环境,不涉及如何连接真机调试。

如果你还遇到了一些其他诡异的问题:


参考资料