Simple NFT Viewer
This example project will show you how to build a simple viewer that will allow you to view NFTs that conform to the NFT and MetadataViews standards.
This tutorial will mostly ignore the C# code that actually displays the NFTs and focus on a high level summary of the steps used.
Overview
When querying the blockchain we utilize four scripts:
_10* [GetCollections.cdc](Cadence/GetCollections.cdc) - Gets a list of Collections that conform to NFT.Collection for a given address_10* [GetNftIdsForCollection.cdc](Cadence/GetNftIdsForCollection.cdc) - Gets a list of all NFT IDs that are contained in a given collection_10* [GetDisplayDataForIDs.cdc](Cadence/GetDisplayDataForIDs.cdc) - Gets just the display data for a given NFT_10* [GetFullDataForID.cdc](Cadence/GetFullDataForID.cdc) - Gets a more comprehensive set of data for a single NFT.
While we could use a single script to query for all the data, larger collections will cause the script to time out. Instead we query for just the data we need to reduce the chances of a timeout occurring.
Finding Collections
First we need to get a list of all collections on an account that are a subtype of NFT.Collection.
_25import NonFungibleToken from 0x1d7e57aa55817448_25_25pub fun main(addr: Address) : [StoragePath] {_25    //Get the AuthAccount for the given address._25    //The AuthAccount is needed because we're going to be looking into the Storage of the user_25    var acct = getAuthAccount(addr)_25    _25    //Array that we will fill with all valid storage paths_25    var paths : [StoragePath] = []_25    _25    //Uses the storage iteration API to iterate through all storage paths on the account_25    acct.forEachStored(fun (path: StoragePath, type:Type): Bool {_25        //Check to see if the resource at this location is a subtype of NonFungibleToken.Collection._25        if type.isSubtype(of: Type<@NonFungibleToken.Collection>()) {_25            //Add this path to the array_25            paths.append(path)_25        }_25        _25        //returning true tells the iterator to continue to the next entry_25        return true_25    });_25    _25    //Return the array that we built_25    return paths_25}
We use the Storage Iteration API to look at everything the account has in it's storage and see if it is an NFT Collection. We return a list of all found NFT Collections.
Getting NFT IDs Contained in a Collection
We use this to create a list of collection paths a user can pick from. When the user selects a path to view, we fetch a list of IDs contained in that collection:
_13import NonFungibleToken from 0x1d7e57aa55817448_13_13pub fun main(addr: Address, path: StoragePath) : [UInt64] {_13    //Get the AuthAccount for the given address._13    //The AuthAccount is needed because we're going to be looking into the Storage of the user_13    var acct = getAuthAccount(addr)_13    _13    //Get a reference to an interface of type NonFungibleToken.Collection public backed by the resource located at path_13    var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!_13    _13    //Return the list of NFT IDs contained in this collection_13    return ref!.getIDs()_13}
Getting Display Data for an NFT
After we get a list of the available NFT IDs, we need to get some basic data about the NFT to display the thumbnail icon.
_30import NonFungibleToken from 0x1d7e57aa55817448_30import MetadataViews from 0x1d7e57aa55817448_30_30pub fun main(addr: Address, path: StoragePath, ids: [UInt64]) : {UInt64:AnyStruct?} {_30    //Array to hold the NFT display data that we will return_30    //We use AnyStruct? because that is the type that is returned by resolveView._30    var returnData: {UInt64:AnyStruct?} = {}_30_30    //Get account for address_30    var acct = getAuthAccount(addr)_30    _30    //Get a reference to a capability to the storage path as a NonFungibleToken.CollectionPublic_30    var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!_30    _30    //Loop through the requested IDs_30    for id in ids {       _30        //Get a reference to the NFT we're interested in_30        var nftRef = ref.borrowNFT(id: id)_30        _30        //If for some reason we couldn't borrow a reference, continue onto the next NFT_30        if nftRef == nil {_30            continue_30        }_30_30        //Fetch the information we're interested in and store it in our NFT structure_30        returnData[id] = nftRef.resolveView(Type<MetadataViews.Display>())_30    }_30    _30    return returnData_30}
This gives us a dictionary that maps NFT IDs to Display structs ({UInt64:MetadataViews.Display}).  Because accessing this
information can be tedious in C#, we can define some C# classes to make our lives easier:
_13public class File_13{_13    public string url;_13    public string cid;_13    public string path;_13}_13_13public class Display_13{_13    public String name;_13    public String description;_13    public File thumbnail;_13}
This will allow us to use Cadence.Convert to convert from the CadenceBase that the script returns into a Display class.
This line in NFTViewer.cs is an example of converting using Cadence.Convert:
_10Dictionary<UInt64, Display> displayData = Convert.FromCadence<Dictionary<UInt64, Display>>(scriptResponseTask.Result.Value);
You might ask whey we don't combine GetNftIdsForCollection.cdc and GetDisplayDataForIDs.cdc to get the Display data at the same time we get the list of IDs. This approach would work in many cases, but when an account contains large numbers of NFTs, this could cause a script timeout. Getting the list of IDs is a cheap call because the NFT contains this list in an array already. By getting just the NFT IDs, we could implement paging and use multiple script calls to each fetch a portion of the display data. This example doesn't currently do this type of paging, but could do so without modifying the cadence scripts.
Getting Complete NFT Data
When a user selects a particular NFT to view in more detail, we need to fetch that detail.
_82import NonFungibleToken from 0x1d7e57aa55817448_82import MetadataViews from 0x1d7e57aa55817448_82_82//Structure that will hold all the data we want for an NFT_82pub struct NFTData {_82    pub(set) var NFTView: AnyStruct?_82    pub(set) var Display : AnyStruct?_82    pub(set) var HTTPFile: AnyStruct?_82    pub(set) var IPFSFile: AnyStruct?_82    pub(set) var Edition: AnyStruct?_82    pub(set) var Editions: AnyStruct?_82    pub(set) var Serial: AnyStruct?_82    pub(set) var Royalty: AnyStruct?_82    pub(set) var Royalties: AnyStruct?_82    pub(set) var Media: AnyStruct?_82    pub(set) var Medias: AnyStruct?_82    pub(set) var License: AnyStruct?_82    pub(set) var ExternalURL: AnyStruct?_82    pub(set) var NFTCollectionDisplay: AnyStruct?_82    pub(set) var Rarity: AnyStruct?_82    pub(set) var Trait: AnyStruct?_82    pub(set) var Traits: AnyStruct?_82    _82    init() {_82        self.NFTView = nil_82        self.Display = nil_82        self.HTTPFile = nil_82        self.IPFSFile = nil_82        self.Edition = nil_82        self.Editions = nil_82        self.Serial = nil_82        self.Royalty = nil_82        self.Royalties = nil_82        self.Media = nil_82        self.Medias = nil_82        self.License = nil_82        self.ExternalURL = nil_82        self.NFTCollectionDisplay = nil_82        self.Rarity = nil_82        self.Trait = nil_82        self.Traits = nil_82    }_82}_82_82pub fun main(addr: Address, path: StoragePath, id: UInt64) : NFTData? {_82    //Get account for address_82    var acct = getAuthAccount(addr)_82    _82    //Get a reference to a capability to the storage path as a NonFungibleToken.CollectionPublic_82    var ref = acct.borrow<&{NonFungibleToken.CollectionPublic}>(from: path)!_82    _82    //Get a reference to the NFT we're interested in_82    var nftRef = ref.borrowNFT(id: id)_82    _82    //If for some reason we couldn't borrow a reference, continue onto the next NFT_82    if nftRef == nil {_82        return nil_82    }_82_82    var nftData : NFTData = NFTData() _82_82    //Fetch the information we're interested in and store it in our NFT structure_82    nftData.Display = nftRef.resolveView(Type<MetadataViews.Display>())_82    nftData.NFTView = nftRef.resolveView(Type<MetadataViews.NFTView>())_82    nftData.HTTPFile = nftRef.resolveView(Type<MetadataViews.HTTPFile>())_82    nftData.IPFSFile = nftRef.resolveView(Type<MetadataViews.IPFSFile>())_82    nftData.Edition = nftRef.resolveView(Type<MetadataViews.Edition>())_82    nftData.Editions = nftRef.resolveView(Type<MetadataViews.Editions>())_82    nftData.Serial = nftRef.resolveView(Type<MetadataViews.Serial>())_82    nftData.Media = nftRef.resolveView(Type<MetadataViews.Media>())_82    nftData.Rarity = nftRef.resolveView(Type<MetadataViews.Rarity>())_82    nftData.Trait = nftRef.resolveView(Type<MetadataViews.Trait>())_82    nftData.Traits = nftRef.resolveView(Type<MetadataViews.Traits>())_82    nftData.Medias = nftRef.resolveView(Type<MetadataViews.Medias>())_82    nftData.ExternalURL = nftRef.resolveView(Type<MetadataViews.ExternalURL>())_82    nftData.Royalty = nftRef.resolveView(Type<MetadataViews.Royalty>())_82    nftData.Royalties = nftRef.resolveView(Type<MetadataViews.Royalties>())_82    nftData.License = nftRef.resolveView(Type<MetadataViews.License>())_82    nftData.NFTCollectionDisplay = nftRef.resolveView(Type<MetadataViews.NFTCollectionDisplay>())_82    _82    return nftData_82}
Here we define a struct NFTData that will contain all the different information we want and fill the struct via multiple resolveView calls.
C# Classes for Easy Converting
The end of NFTViewer.cs contains classes that we use to more easily convert from Cadence into C#. One thing to note is that the Cadence structs contain Optionals, like:
var IPFSFile: AnyStruct?
while the C# versions do not, such as
public IPFSFile IPFSFile;
This is because we are declaring them as Classes, not Structs. Classes in C# are reference types, which can automatically be null. We could have used Structs, in which case we'd have to use:
public IPFSFile? IPFSFile
This would wrap the IPFSFile struct in a Nullable, which would allow it to be null if the Cadence value was nil.
Another thing to note is the declaration of the C# File class:
_16public class File_16{_16    public string url;_16    public string cid;_16    public string path;_16_16    public string GetURL()_16    {_16        if (string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(cid))_16        {_16            return $"https://ipfs.io/ipfs/{cid}"; _16        }_16_16        return url;_16    }_16}
Compare this to the File interface in the MetadataViews contract:
_10    pub struct interface File {_10        pub fun uri(): String_10    }
The MetadataViews.File interface doesn't actually contain any fields, only a single method. Because only two things in MetadataViews implement the File interface (HTTPFile and IPFSFile), we chose to combine the possible fields into our File class.
_10pub struct HTTPFile: File {_10        pub let url: String_10}_10_10pub struct IPFSFile: File {_10    pub let cid: String_10    pub let path: String?_10}
This allows Cadence.Convert to convert either an HTTPFile or an IPFSFile into a File object. We can then check which fields are populated to determine which it was initially.
This works fine for this simple viewer, but a more robust approach might be to create a ResolvedFile struct in the cadence script which has a single uri field and populates it by calling the uri() function on whatever File type was retrieved.